@codehz/ai 0.2.0 → 0.2.1

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,45 @@ 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
+ };
488
+ }
489
+ function getActiveItem(state, itemId, expectedType) {
490
+ const item = state.activeItems.get(itemId);
491
+ if (!item) throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
492
+ if (item.type !== expectedType) throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
493
+ return item;
494
+ }
495
+ function finalizeMessage(active) {
496
+ return {
497
+ type: "message",
498
+ id: active.id,
499
+ role: active.role,
500
+ content: active.content
501
+ };
502
+ }
503
+ function finalizeReasoning(active) {
504
+ return {
505
+ type: "reasoning",
506
+ id: active.id,
507
+ visibility: active.visibility,
508
+ content: active.content
509
+ };
510
+ }
511
+ function finalizeToolCall(active) {
512
+ return {
513
+ type: "tool_call",
514
+ id: active.id,
515
+ name: active.name,
516
+ argumentsText: active.argumentsText
458
517
  };
459
518
  }
460
519
  function handleResponseStarted(state, event) {
520
+ if (state.started) throw streamProtocolError("Stream must contain exactly one response.started event");
521
+ state.started = true;
461
522
  state.responseId = event.responseId;
462
523
  state.model = event.model;
463
524
  state.backendInfo = event.backend;
@@ -474,47 +535,97 @@ function handleResponseAuxiliary(state, event) {
474
535
  ...state.billing,
475
536
  ...event.billing
476
537
  };
477
- if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
538
+ if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
539
+ }
540
+ function handleMessageStarted(state, event) {
541
+ const id = event.item.id;
542
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
543
+ state.activeItems.set(id, {
544
+ type: "message",
545
+ id,
546
+ role: event.item.role,
547
+ content: []
548
+ });
549
+ }
550
+ function handleMessageDelta(state, event) {
551
+ getActiveItem(state, event.itemId, "message").content.push(event.delta);
478
552
  }
479
553
  function handleMessageCompleted(state, event) {
480
- state.output.push(event.item);
481
- pushMessageText(state, event.item);
554
+ const active = getActiveItem(state, event.itemId, "message");
555
+ state.activeItems.delete(event.itemId);
556
+ const item = finalizeMessage(active);
557
+ state.output.push(item);
558
+ pushMessageText(state, item);
559
+ }
560
+ function handleReasoningStarted(state, event) {
561
+ const id = event.item.id;
562
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
563
+ state.activeItems.set(id, {
564
+ type: "reasoning",
565
+ id,
566
+ visibility: event.item.visibility,
567
+ content: []
568
+ });
569
+ }
570
+ function handleReasoningDelta(state, event) {
571
+ getActiveItem(state, event.itemId, "reasoning").content.push(event.delta);
482
572
  }
483
573
  function handleReasoningCompleted(state, event) {
484
- state.output.push(event.item);
574
+ const active = getActiveItem(state, event.itemId, "reasoning");
575
+ state.activeItems.delete(event.itemId);
576
+ state.output.push(finalizeReasoning(active));
577
+ }
578
+ function handleToolCallStarted(state, event) {
579
+ const id = event.item.id;
580
+ if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
581
+ state.activeItems.set(id, {
582
+ type: "tool_call",
583
+ id,
584
+ name: event.item.name,
585
+ argumentsText: ""
586
+ });
587
+ }
588
+ function handleToolCallDelta(state, event) {
589
+ const active = getActiveItem(state, event.itemId, "tool_call");
590
+ if (event.delta.argumentsText) active.argumentsText += event.delta.argumentsText;
485
591
  }
486
592
  function handleToolCallCompleted(state, event) {
487
- state.output.push(event.item);
488
- state.toolCalls.push(event.item);
593
+ const active = getActiveItem(state, event.itemId, "tool_call");
594
+ state.activeItems.delete(event.itemId);
595
+ const item = finalizeToolCall(active);
596
+ state.output.push(item);
597
+ state.toolCalls.push(item);
489
598
  }
490
599
  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 = {
600
+ if (state.activeItems.size > 0) throw streamProtocolError("response.completed received while active items still pending");
601
+ state.completed = true;
602
+ state.replayFromAdapter = event.replay;
603
+ state.stopReasonFromAdapter = event.stopReason;
604
+ state.backendFromAdapter = event.trace;
605
+ if (event.usage) state.usage = {
496
606
  ...state.usage,
497
- ...event.response.usage
607
+ ...event.usage
498
608
  };
499
- if (event.response.billing) state.billing = {
609
+ if (event.billing) state.billing = {
500
610
  ...state.billing,
501
- ...event.response.billing
611
+ ...event.billing
502
612
  };
503
- if (event.response.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.response.auxiliary);
504
- if (event.response.warnings) pushWarnings(state, event.response.warnings);
613
+ if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
614
+ if (event.warnings) pushWarnings(state, event.warnings);
615
+ if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
505
616
  }
506
617
  function buildResponse(state) {
507
- const backendFromResponse = state.backendFromAdapter;
618
+ const backendFromCompleted = state.backendFromAdapter;
508
619
  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
620
+ adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? "unknown",
621
+ isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
622
+ requestId: backendFromCompleted?.requestId ?? state.responseId,
623
+ rawResponseId: backendFromCompleted?.rawResponseId,
624
+ metadataSources: backendFromCompleted?.metadataSources,
625
+ warnings: backendFromCompleted?.warnings
515
626
  };
516
627
  return {
517
- id: state.responseIdFromAdapter ?? state.responseId,
628
+ id: state.responseId,
518
629
  output: state.output,
519
630
  replay: state.replayFromAdapter ?? [],
520
631
  text: state.textParts.join(""),
@@ -537,6 +648,7 @@ function aggregateEvents(events) {
537
648
  return finalizeAggregation(state);
538
649
  }
539
650
  function aggregateEvent(state, event) {
651
+ validateEventEnvelope(state, event);
540
652
  state.lastEventType = event.type;
541
653
  switch (event.type) {
542
654
  case "response.started":
@@ -549,17 +661,29 @@ function aggregateEvent(state, event) {
549
661
  handleResponseAuxiliary(state, event);
550
662
  break;
551
663
  case "message.started":
664
+ handleMessageStarted(state, event);
665
+ break;
552
666
  case "message.delta":
553
- case "reasoning.started":
554
- case "reasoning.delta":
555
- case "tool_call.started":
556
- case "tool_call.delta": break;
667
+ handleMessageDelta(state, event);
668
+ break;
557
669
  case "message.completed":
558
670
  handleMessageCompleted(state, event);
559
671
  break;
672
+ case "reasoning.started":
673
+ handleReasoningStarted(state, event);
674
+ break;
675
+ case "reasoning.delta":
676
+ handleReasoningDelta(state, event);
677
+ break;
560
678
  case "reasoning.completed":
561
679
  handleReasoningCompleted(state, event);
562
680
  break;
681
+ case "tool_call.started":
682
+ handleToolCallStarted(state, event);
683
+ break;
684
+ case "tool_call.delta":
685
+ handleToolCallDelta(state, event);
686
+ break;
563
687
  case "tool_call.completed":
564
688
  handleToolCallCompleted(state, event);
565
689
  break;
@@ -569,20 +693,10 @@ function aggregateEvent(state, event) {
569
693
  }
570
694
  }
571
695
  function finalizeAggregation(state) {
572
- if (state.lastEventType !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
696
+ if (!state.started) throw streamProtocolError("Stream must start with response.started event");
697
+ if (!state.completed || state.lastEventType !== "response.completed") throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
573
698
  return buildResponse(state);
574
699
  }
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
700
  function pushWarnings(state, warnings) {
587
701
  for (const warning of warnings) if (!state.warningSet.has(warning)) {
588
702
  state.warningSet.add(warning);
@@ -592,6 +706,16 @@ function pushWarnings(state, warnings) {
592
706
  function pushMessageText(state, item) {
593
707
  for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
594
708
  }
709
+ function validateEventEnvelope(state, event) {
710
+ if (state.completed) throw streamProtocolError("response.completed must be the final stream event");
711
+ if (!state.started && event.type !== "response.started") throw streamProtocolError("Stream must start with response.started event");
712
+ if (state.responseId !== void 0 && event.responseId !== state.responseId) throw streamProtocolError("All stream events must use the same responseId");
713
+ if (state.nextSequence !== void 0 && event.sequence !== state.nextSequence) throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
714
+ state.nextSequence = event.sequence + 1;
715
+ }
716
+ function streamProtocolError(message) {
717
+ return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
718
+ }
595
719
  //#endregion
596
720
  //#region src/core/collect-stream.ts
597
721
  async function collectStream(stream) {
@@ -937,7 +1061,7 @@ var AdapterBase = class {
937
1061
  responseId: request.requestId,
938
1062
  backend: {
939
1063
  kind: this.kind,
940
- isSynthetic: !this.nativeStreaming
1064
+ isSynthetic: this.capabilities.textStreaming === "synthetic"
941
1065
  }
942
1066
  });
943
1067
  yield factory.responseStarted(request.model);
@@ -945,12 +1069,25 @@ var AdapterBase = class {
945
1069
  const providerRequest = await this.buildRequest(request);
946
1070
  yield* this.runStream(providerRequest, factory, request);
947
1071
  } 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));
1072
+ if (err instanceof AIRequestError || err instanceof AIProviderError || err instanceof AIStreamError) throw err;
1073
+ if (err instanceof AIMappingError) {
1074
+ yield factory.responseWarning(err.message, "MAPPING_ERROR");
1075
+ const errorResp = this.buildResponse(request, {
1076
+ output: [],
1077
+ replay: []
1078
+ }, factory);
1079
+ yield factory.responseCompleted({
1080
+ replay: errorResp.replay,
1081
+ stopReason: errorResp.stopReason,
1082
+ trace: errorResp.backend,
1083
+ usage: errorResp.usage,
1084
+ billing: errorResp.billing,
1085
+ auxiliary: errorResp.auxiliary,
1086
+ warnings: errorResp.warnings
1087
+ });
1088
+ return;
1089
+ }
1090
+ throw err;
954
1091
  }
955
1092
  }
956
1093
  /**
@@ -976,7 +1113,7 @@ var AdapterBase = class {
976
1113
  requestId: request.requestId,
977
1114
  rawResponseId: result.rawResponseId,
978
1115
  adapter: this.kind,
979
- isSyntheticStream: !this.nativeStreaming,
1116
+ isSyntheticStream: this.capabilities.textStreaming === "synthetic",
980
1117
  metadataSources: result.metadataSources,
981
1118
  warnings
982
1119
  }
@@ -990,18 +1127,6 @@ var AdapterBase = class {
990
1127
  return new AdapterAuxiliaryState(request);
991
1128
  }
992
1129
  };
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
1130
  function mergeWarnings(...groups) {
1006
1131
  const merged = [];
1007
1132
  for (const group of groups) {
@@ -1011,6 +1136,91 @@ function mergeWarnings(...groups) {
1011
1136
  return merged.length > 0 ? merged : void 0;
1012
1137
  }
1013
1138
  //#endregion
1139
+ //#region src/helpers/adapter-security.ts
1140
+ /**
1141
+ * Adapter 边界安全辅助
1142
+ *
1143
+ * - opaque replay 入站 envelope(大小 / 深度)
1144
+ * - provider HTTP 错误 body 出站脱敏
1145
+ */
1146
+ const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
1147
+ const MAX_OPAQUE_JSON_DEPTH = 8;
1148
+ const PROVIDER_ERROR_MESSAGE_MAX_LEN = 500;
1149
+ const PROVIDER_ERROR_RAW_BODY_THRESHOLD = 200;
1150
+ /** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
1151
+ function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
1152
+ if (value === null || typeof value !== "object") return 0;
1153
+ if (seen.has(value)) return 0;
1154
+ seen.add(value);
1155
+ let maxChild = 0;
1156
+ if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
1157
+ else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
1158
+ return 1 + maxChild;
1159
+ }
1160
+ /**
1161
+ * Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
1162
+ * 不校验 adapter 专用字段形状。
1163
+ */
1164
+ function validateOpaqueReplayEnvelope(payload) {
1165
+ if (typeof payload !== "object" || payload === null) return {
1166
+ ok: false,
1167
+ reason: "payload must be an object"
1168
+ };
1169
+ let raw;
1170
+ try {
1171
+ raw = JSON.stringify(payload);
1172
+ } catch {
1173
+ return {
1174
+ ok: false,
1175
+ reason: "payload is not JSON-serializable"
1176
+ };
1177
+ }
1178
+ if (raw === void 0) return {
1179
+ ok: false,
1180
+ reason: "payload is not JSON-serializable"
1181
+ };
1182
+ if (raw.length > 65536) return {
1183
+ ok: false,
1184
+ reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`
1185
+ };
1186
+ const depth = measureJsonDepth(payload);
1187
+ if (depth > 8) return {
1188
+ ok: false,
1189
+ reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
1190
+ };
1191
+ return { ok: true };
1192
+ }
1193
+ /** envelope 失败时抛 AIRequestError。 */
1194
+ function assertOpaqueReplayEnvelope(payload) {
1195
+ const result = validateOpaqueReplayEnvelope(payload);
1196
+ if (!result.ok) throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
1197
+ }
1198
+ /**
1199
+ * 从 provider HTTP 错误 body 提取可对外暴露的短消息,避免泄漏 HTML / 内部路径等。
1200
+ */
1201
+ function extractProviderErrorMessage(body, status) {
1202
+ if (!body) return `HTTP ${status}`;
1203
+ try {
1204
+ const parsed = JSON.parse(body);
1205
+ if (parsed && typeof parsed === "object") {
1206
+ const record = parsed;
1207
+ const errorField = record.error;
1208
+ let msg;
1209
+ if (errorField && typeof errorField === "object" && errorField !== null) msg = errorField.message;
1210
+ if (typeof msg !== "string") msg = typeof errorField === "string" ? errorField : record.message;
1211
+ if (typeof msg === "string" && msg.length > 0) return msg.slice(0, 500);
1212
+ }
1213
+ } catch {}
1214
+ const trimmed = body.trimStart();
1215
+ if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || body.length > 200) return `HTTP ${status}. Body omitted (${body.length} bytes)`;
1216
+ return body.slice(0, 500);
1217
+ }
1218
+ /** 统一构造脱敏后的 AIProviderError。 */
1219
+ function providerHttpError(status, body) {
1220
+ const safe = extractProviderErrorMessage(body, status);
1221
+ return new AIProviderError(`Provider returned ${status}: ${safe}`, "PROVIDER_ERROR", status, safe);
1222
+ }
1223
+ //#endregion
1014
1224
  //#region src/helpers/usage-mapping.ts
1015
1225
  function num(value) {
1016
1226
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
@@ -1090,43 +1300,55 @@ function usageFromOllama(raw) {
1090
1300
  * - 未完成的行保留在 rest 中,等待下次 chunk 补全
1091
1301
  * - 支持跨 chunk 的 event 分片
1092
1302
  */
1093
- function parseSSEEvents(chunk) {
1303
+ function parseSSEEvents(chunk, options = {}) {
1094
1304
  const events = [];
1095
1305
  let eventType = "";
1096
1306
  let dataLines = [];
1097
1307
  let consumedUntil = 0;
1098
1308
  let cursor = 0;
1099
1309
  let malformedEvents = 0;
1310
+ const emitEvent = (consumedCursor) => {
1311
+ const dataStr = dataLines.join("\n");
1312
+ if (dataStr === "[DONE]") {
1313
+ eventType = "";
1314
+ dataLines = [];
1315
+ consumedUntil = consumedCursor;
1316
+ return;
1317
+ }
1318
+ try {
1319
+ const data = JSON.parse(dataStr);
1320
+ events.push({
1321
+ type: eventType,
1322
+ data
1323
+ });
1324
+ } catch {
1325
+ malformedEvents++;
1326
+ }
1327
+ eventType = "";
1328
+ dataLines = [];
1329
+ consumedUntil = consumedCursor;
1330
+ };
1331
+ const consumeLine = (line, consumedCursor) => {
1332
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1333
+ else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1334
+ else if (line === "" && eventType && dataLines.length > 0) emitEvent(consumedCursor);
1335
+ else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = consumedCursor;
1336
+ };
1100
1337
  while (cursor < chunk.length) {
1101
1338
  const lineEnd = chunk.indexOf("\n", cursor);
1102
1339
  if (lineEnd === -1) break;
1103
1340
  let line = chunk.slice(cursor, lineEnd);
1104
1341
  cursor = lineEnd + 1;
1105
1342
  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;
1343
+ consumeLine(line, cursor);
1344
+ }
1345
+ if (options.allowEOF && cursor < chunk.length) {
1346
+ let line = chunk.slice(cursor);
1347
+ if (line.endsWith("\r")) line = line.slice(0, -1);
1348
+ consumeLine(line, chunk.length);
1349
+ cursor = chunk.length;
1129
1350
  }
1351
+ if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
1130
1352
  return {
1131
1353
  events,
1132
1354
  rest: chunk.slice(consumedUntil),
@@ -1134,71 +1356,288 @@ function parseSSEEvents(chunk) {
1134
1356
  };
1135
1357
  }
1136
1358
  //#endregion
1137
- //#region src/adapters/responses.ts
1359
+ //#region src/helpers/synthetic-stream.ts
1138
1360
  /**
1139
- * Responses Adapter
1361
+ * 模拟流式 (Synthetic Streaming)
1140
1362
  *
1141
- * 接入 OpenAI Responses API (responses 端点)。
1142
- * 职责分层:
1143
- * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1144
- * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1363
+ * 将一组已解析的 canonical OutputItem 包装为规范事件流。
1364
+ * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
1365
+ * 即可产出一致的事件序列,无需自己逐事件组装。
1145
1366
  *
1146
- * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1367
+ * 约束:
1368
+ * - 每个 item 只发一块完整 delta(不模拟逐 token)
1369
+ * - 保持 item 边界
1370
+ * - 保持后端原始顺序
1371
+ * - 不发明 reasoning
1372
+ * - 不改写工具参数
1373
+ */
1374
+ /**
1375
+ * 将已解析的 output items 包装为完整规范事件流。
1376
+ *
1377
+ * 用法示例(在 adapter 的 runStream 中):
1378
+ * ```ts
1379
+ * const result = parseNonStreamingResponse(data);
1380
+ * yield* syntheticStream({
1381
+ * model: request.model,
1382
+ * responseId: request.requestId,
1383
+ * backend: { kind: "chat-completions" },
1384
+ * output: result.output,
1385
+ * stopReason: result.stopReason,
1386
+ * usage: result.usage,
1387
+ * });
1388
+ * ```
1147
1389
  */
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;
1390
+ async function* syntheticStream(options) {
1391
+ const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
1392
+ const factory = createEventFactory({
1393
+ responseId,
1394
+ backend: {
1395
+ kind: backend.kind,
1396
+ isSynthetic: true
1397
+ }
1398
+ });
1399
+ yield factory.responseStarted(model);
1400
+ for (const item of output) yield* emitItemEvents(item, factory);
1401
+ if (usage || billing) yield factory.responseAuxiliary({
1402
+ usage,
1403
+ billing
1404
+ });
1405
+ const finalReplay = replay ?? replayFromOutput(output);
1406
+ const allWarnings = [];
1407
+ allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
1408
+ if (extraWarnings) allWarnings.push(...extraWarnings);
1409
+ yield factory.responseCompleted({
1410
+ replay: finalReplay,
1411
+ stopReason,
1412
+ usage,
1413
+ billing,
1414
+ auxiliary: providerMetadata ? { providerMetadata } : void 0,
1415
+ opaqueOutput: output.filter((item) => item.type === "opaque"),
1416
+ warnings: allWarnings.length > 0 ? allWarnings : void 0,
1417
+ trace: {
1418
+ requestId: responseId,
1419
+ rawResponseId,
1420
+ adapter: backend.kind,
1421
+ isSyntheticStream: true,
1422
+ warnings: allWarnings.length > 0 ? allWarnings : void 0
1423
+ }
1160
1424
  });
1161
1425
  }
1162
- function instructionsToResponsesText(instructions) {
1163
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
1426
+ function* emitItemEvents(item, factory) {
1427
+ switch (item.type) {
1428
+ case "message":
1429
+ yield* emitMessageEvents(item, factory);
1430
+ break;
1431
+ case "reasoning":
1432
+ yield* emitReasoningEvents(item, factory);
1433
+ break;
1434
+ case "tool_call":
1435
+ yield* emitToolCallEvents(item, factory);
1436
+ break;
1437
+ case "opaque": break;
1438
+ }
1164
1439
  }
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");
1440
+ function* emitMessageEvents(item, factory) {
1441
+ const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
1442
+ yield factory.messageStarted(id);
1443
+ for (const block of item.content) yield factory.messageDelta(id, block);
1444
+ yield factory.messageCompleted(id);
1167
1445
  }
1168
- function parseSSE(chunk) {
1169
- const result = parseSSEEvents(chunk);
1170
- return {
1171
- events: result.events,
1172
- rest: result.rest,
1173
- malformedEvents: result.malformedEvents
1174
- };
1446
+ function* emitReasoningEvents(item, factory) {
1447
+ const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
1448
+ yield factory.reasoningStarted(id, item.visibility);
1449
+ for (const block of item.content) yield factory.reasoningDelta(id, block);
1450
+ yield factory.reasoningCompleted(id);
1175
1451
  }
1176
- function isReplayCanonicalInput(item) {
1177
- return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
1452
+ function* emitToolCallEvents(item, factory) {
1453
+ yield factory.toolCallStarted(item.id, item.name);
1454
+ if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
1455
+ yield factory.toolCallCompleted(item.id);
1178
1456
  }
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();
1457
+ //#endregion
1458
+ //#region src/helpers/incremental-stream-parser.ts
1459
+ var IncrementalStreamParser = class {
1460
+ split;
1461
+ parse;
1462
+ buffer = "";
1463
+ decoder = new TextDecoder();
1464
+ constructor(split, parse) {
1465
+ this.split = split;
1466
+ this.parse = parse;
1467
+ }
1468
+ feed(value) {
1469
+ this.buffer += this.decoder.decode(value, { stream: true });
1470
+ return this.consume(false);
1471
+ }
1472
+ flush() {
1473
+ this.buffer += this.decoder.decode();
1474
+ return this.consume(true);
1475
+ }
1476
+ getRemaining() {
1477
+ return this.buffer;
1478
+ }
1479
+ consume(allowEOF) {
1480
+ const split = this.split(this.buffer, allowEOF);
1481
+ this.buffer = split.rest;
1482
+ const items = [];
1483
+ let malformed = 0;
1484
+ for (const rawItem of split.items) {
1485
+ const result = this.parse(rawItem);
1486
+ if (result.status === "parsed") items.push(result.value);
1487
+ else if (result.status === "malformed") malformed++;
1488
+ }
1489
+ return {
1490
+ items,
1491
+ malformed
1492
+ };
1184
1493
  }
1185
- }
1186
- function canonicalToResponsesBlock(b) {
1187
- if (b.type === "text") return {
1188
- type: "text",
1189
- text: b.text
1494
+ };
1495
+ function splitLines(buffer, allowEOF) {
1496
+ const items = [];
1497
+ let cursor = 0;
1498
+ while (true) {
1499
+ const lineEnd = buffer.indexOf("\n", cursor);
1500
+ if (lineEnd === -1) break;
1501
+ items.push(buffer.slice(cursor, lineEnd));
1502
+ cursor = lineEnd + 1;
1503
+ }
1504
+ if (allowEOF && cursor < buffer.length) {
1505
+ items.push(buffer.slice(cursor));
1506
+ cursor = buffer.length;
1507
+ }
1508
+ return {
1509
+ items,
1510
+ rest: buffer.slice(cursor)
1190
1511
  };
1191
- if (b.type === "json") return {
1192
- type: "text",
1193
- text: JSON.stringify(b.json)
1512
+ }
1513
+ function splitSSEFrames(buffer, allowEOF) {
1514
+ const normalized = buffer.replaceAll("\r\n", "\n");
1515
+ const items = [];
1516
+ let cursor = 0;
1517
+ while (true) {
1518
+ const frameEnd = normalized.indexOf("\n\n", cursor);
1519
+ if (frameEnd === -1) break;
1520
+ items.push(normalized.slice(cursor, frameEnd));
1521
+ cursor = frameEnd + 2;
1522
+ }
1523
+ if (allowEOF && cursor < normalized.length) {
1524
+ items.push(normalized.slice(cursor));
1525
+ cursor = normalized.length;
1526
+ }
1527
+ return {
1528
+ items,
1529
+ rest: normalized.slice(cursor)
1194
1530
  };
1195
- throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1196
1531
  }
1197
- var ResponsesAdapter = class extends AdapterBase {
1198
- kind = "responses";
1199
- nativeStreaming = true;
1200
- apiKey;
1201
- baseUrl;
1532
+ //#endregion
1533
+ //#region src/helpers/request-mapper.ts
1534
+ var NormalizedRequestMapper = class {
1535
+ profile;
1536
+ constructor(profile) {
1537
+ this.profile = profile;
1538
+ }
1539
+ mapInstructions(instructions) {
1540
+ return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
1541
+ }
1542
+ ensureTextBlocks(blocks, field) {
1543
+ return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
1544
+ }
1545
+ ensureReasoningBlocks(blocks, field) {
1546
+ return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
1547
+ }
1548
+ assertToolResultOutcome(outcome) {
1549
+ if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
1550
+ const outcomes = this.profile.capabilities.toolResultOutcomes;
1551
+ const supported = outcomes.map((value) => `"${value}"`).join(" and ");
1552
+ const verb = outcomes.length > 1 ? "are" : "is";
1553
+ throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1554
+ }
1555
+ rollbackTrailingAssistantMessages(messages) {
1556
+ while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1557
+ }
1558
+ ensureBlocks(blocks, field, supportedTypes, description) {
1559
+ for (let i = 0; i < blocks.length; i++) {
1560
+ const block = blocks[i];
1561
+ 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");
1562
+ }
1563
+ return blocks;
1564
+ }
1565
+ };
1566
+ //#endregion
1567
+ //#region src/adapters/responses.ts
1568
+ /**
1569
+ * Responses Adapter
1570
+ *
1571
+ * 接入 OpenAI Responses API (responses 端点)。
1572
+ * 职责分层:
1573
+ * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1574
+ * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1575
+ *
1576
+ * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1577
+ */
1578
+ const profile$3 = {
1579
+ kind: "responses",
1580
+ instructionsMode: "instructions_field",
1581
+ supportedBlockTypes: ["text", "json"],
1582
+ reasoningBlockTypes: ["text"],
1583
+ capabilities: {
1584
+ textStreaming: "native",
1585
+ reasoningStreaming: "native",
1586
+ toolCallStreaming: "native",
1587
+ replay: "opaque",
1588
+ usage: "final",
1589
+ toolResultOutcomes: ["success"]
1590
+ }
1591
+ };
1592
+ const mapper$3 = new NormalizedRequestMapper(profile$3);
1593
+ /** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
1594
+ const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1595
+ "response.output_item.added",
1596
+ "response.output_item.done",
1597
+ "response.output_text.delta",
1598
+ "response.output_text.done",
1599
+ "response.reasoning.delta",
1600
+ "response.reasoning.done",
1601
+ "response.tool_call.delta",
1602
+ "response.tool_call.done",
1603
+ "response.function_call_arguments.delta",
1604
+ "response.function_call_arguments.done",
1605
+ "response.content_part.added",
1606
+ "response.content_part.done",
1607
+ "response.refusal.delta",
1608
+ "response.refusal.done",
1609
+ "response.in_progress",
1610
+ "response.created",
1611
+ "response.completed",
1612
+ "response.failed",
1613
+ "response.incomplete",
1614
+ "error"
1615
+ ]);
1616
+ function isReplayCanonicalInput(item) {
1617
+ return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
1618
+ }
1619
+ function hasReplayCanonicalInput(input) {
1620
+ return input.some(isReplayCanonicalInput);
1621
+ }
1622
+ function extractFailureMessage(response) {
1623
+ return response.error?.message ?? response.failure?.message ?? "unknown";
1624
+ }
1625
+ function canonicalToResponsesBlock(b) {
1626
+ if (b.type === "text") return {
1627
+ type: "text",
1628
+ text: b.text
1629
+ };
1630
+ if (b.type === "json") return {
1631
+ type: "text",
1632
+ text: JSON.stringify(b.json)
1633
+ };
1634
+ throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1635
+ }
1636
+ var ResponsesAdapter = class extends AdapterBase {
1637
+ kind = "responses";
1638
+ capabilities = profile$3.capabilities;
1639
+ apiKey;
1640
+ baseUrl;
1202
1641
  fetchFn;
1203
1642
  constructor(options) {
1204
1643
  super();
@@ -1211,7 +1650,7 @@ var ResponsesAdapter = class extends AdapterBase {
1211
1650
  for (const item of request.input) switch (item.type) {
1212
1651
  case "message":
1213
1652
  if (item.role === "assistant") {
1214
- const blocks = ensureResponsesTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
1653
+ const blocks = mapper$3.ensureTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
1215
1654
  input.push({
1216
1655
  type: "message",
1217
1656
  role: item.role,
@@ -1220,11 +1659,11 @@ var ResponsesAdapter = class extends AdapterBase {
1220
1659
  } else input.push({
1221
1660
  type: "message",
1222
1661
  role: item.role,
1223
- content: contentBlocksToText(ensureResponsesTextBlocks(item.content, `input message (${item.role}) content`))
1662
+ content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
1224
1663
  });
1225
1664
  break;
1226
1665
  case "reasoning": {
1227
- const blocks = ensureResponsesReasoningBlocks(item.content, "reasoning content").map((b) => ({
1666
+ const blocks = mapper$3.ensureReasoningBlocks(item.content, "reasoning content").map((b) => ({
1228
1667
  type: "reasoning",
1229
1668
  text: b.text
1230
1669
  }));
@@ -1243,8 +1682,8 @@ var ResponsesAdapter = class extends AdapterBase {
1243
1682
  });
1244
1683
  break;
1245
1684
  case "tool_result": {
1246
- assertResponsesToolResultOutcome(item.outcome);
1247
- const output = ensureResponsesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1685
+ mapper$3.assertToolResultOutcome(item.outcome);
1686
+ const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1248
1687
  input.push({
1249
1688
  type: "function_call_output",
1250
1689
  call_id: item.callId,
@@ -1252,25 +1691,26 @@ var ResponsesAdapter = class extends AdapterBase {
1252
1691
  });
1253
1692
  break;
1254
1693
  }
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
- }
1694
+ case "opaque": {
1695
+ if (item.source !== "responses" || item.purpose !== "replay") break;
1696
+ assertOpaqueReplayEnvelope(item.payload);
1697
+ const payload = item.payload;
1698
+ if ("id" in payload) {
1699
+ 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");
1700
+ if (!hasReplayCanonicalInput(input)) input.push({
1701
+ type: "item_reference",
1702
+ id: payload.id
1703
+ });
1265
1704
  }
1266
1705
  break;
1706
+ }
1267
1707
  }
1268
1708
  const body = {
1269
1709
  model: request.model,
1270
1710
  input,
1271
1711
  stream: true
1272
1712
  };
1273
- if (request.instructions) body.instructions = instructionsToResponsesText(request.instructions);
1713
+ if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
1274
1714
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1275
1715
  type: "function",
1276
1716
  name: t.name,
@@ -1292,31 +1732,59 @@ var ResponsesAdapter = class extends AdapterBase {
1292
1732
  }
1293
1733
  async *runStream(providerRequest, factory, request) {
1294
1734
  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
- });
1735
+ let response;
1736
+ try {
1737
+ response = await this.fetchFn(`${this.baseUrl}/responses`, {
1738
+ method: "POST",
1739
+ headers: {
1740
+ "Content-Type": "application/json",
1741
+ Authorization: `Bearer ${this.apiKey}`
1742
+ },
1743
+ body: JSON.stringify(providerRequest)
1744
+ });
1745
+ } catch (err) {
1746
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
1747
+ }
1303
1748
  if (!response.ok) {
1304
- const errorText = await response.text().catch(() => "unknown error");
1305
- throw new Error(`Responses API error ${response.status}: ${errorText}`);
1749
+ const errorBody = await response.text().catch(() => "");
1750
+ throw providerHttpError(response.status, errorBody);
1306
1751
  }
1307
1752
  const reader = response.body?.getReader();
1308
- if (!reader) throw new Error("Response body is not readable");
1753
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
1754
+ const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
1755
+ let eventType = "";
1756
+ let dataStr = "";
1757
+ for (const rawLine of frame.split("\n")) {
1758
+ const line = rawLine.trim();
1759
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1760
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
1761
+ }
1762
+ if (!eventType) return { status: "ignored" };
1763
+ try {
1764
+ const data = JSON.parse(dataStr);
1765
+ return {
1766
+ status: "parsed",
1767
+ value: {
1768
+ type: eventType,
1769
+ data
1770
+ }
1771
+ };
1772
+ } catch {
1773
+ return { status: "malformed" };
1774
+ }
1775
+ });
1309
1776
  const output = [];
1310
- const decoder = new TextDecoder();
1311
- let buffer = "";
1777
+ let streamDone = false;
1312
1778
  let completedResponse;
1779
+ let completedEmitted = false;
1780
+ let unknownEventsWarned = false;
1781
+ const messageItemsWithDelta = /* @__PURE__ */ new Set();
1313
1782
  try {
1314
1783
  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;
1784
+ const { done, value } = await reader.read().catch((err) => {
1785
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
1786
+ });
1787
+ const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
1320
1788
  const malformedWarning = emitMalformedStreamWarning(factory, {
1321
1789
  count: malformedEvents,
1322
1790
  providerLabel: "Responses",
@@ -1325,7 +1793,8 @@ var ResponsesAdapter = class extends AdapterBase {
1325
1793
  if (malformedWarning) yield malformedWarning;
1326
1794
  for (const sseEvent of events) {
1327
1795
  if (sseEvent.type === "error") {
1328
- yield factory.responseWarning(sseEvent.data.message, sseEvent.data.code);
1796
+ const data = sseEvent.data;
1797
+ yield factory.responseWarning(data.message ?? "Provider error event", data.code);
1329
1798
  continue;
1330
1799
  }
1331
1800
  if (sseEvent.type === "response.output_item.added") {
@@ -1344,40 +1813,69 @@ var ResponsesAdapter = class extends AdapterBase {
1344
1813
  continue;
1345
1814
  }
1346
1815
  if (sseEvent.type === "response.output_text.delta") {
1347
- yield factory.messageDelta(sseEvent.data.item_id, sseEvent.data.delta);
1816
+ const data = sseEvent.data;
1817
+ yield factory.messageDelta(data.item_id, textBlock(data.delta));
1818
+ messageItemsWithDelta.add(data.item_id);
1348
1819
  continue;
1349
1820
  }
1350
1821
  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 }));
1822
+ const data = sseEvent.data;
1823
+ if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
1824
+ yield factory.messageCompleted(data.item_id);
1825
+ output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
1353
1826
  continue;
1354
1827
  }
1355
1828
  if (sseEvent.type === "response.reasoning.delta") {
1356
- yield factory.reasoningDelta(sseEvent.data.item_id, textBlock(sseEvent.data.delta));
1829
+ const data = sseEvent.data;
1830
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
1357
1831
  continue;
1358
1832
  }
1359
1833
  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));
1834
+ const data = sseEvent.data;
1835
+ yield factory.reasoningCompleted(data.item_id);
1836
+ output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
1362
1837
  continue;
1363
1838
  }
1364
1839
  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 });
1840
+ const data = sseEvent.data;
1841
+ if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
1366
1842
  continue;
1367
1843
  }
1368
1844
  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);
1845
+ const data = sseEvent.data;
1846
+ const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
1847
+ yield factory.toolCallCompleted(data.item_id);
1371
1848
  output.push(tcItem);
1372
1849
  continue;
1373
1850
  }
1374
- if (sseEvent.type === "response.completed") completedResponse = sseEvent.data.response;
1851
+ if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
1852
+ const data = sseEvent.data;
1853
+ if (completedResponse) {
1854
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
1855
+ continue;
1856
+ }
1857
+ completedResponse = data.response;
1858
+ if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
1859
+ continue;
1860
+ }
1861
+ if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
1862
+ unknownEventsWarned = true;
1863
+ yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
1864
+ }
1865
+ }
1866
+ if (done) {
1867
+ streamDone = true;
1868
+ break;
1375
1869
  }
1376
1870
  }
1377
1871
  } finally {
1378
- reader.releaseLock();
1872
+ try {
1873
+ if (!streamDone) await reader.cancel().catch(() => void 0);
1874
+ } finally {
1875
+ reader.releaseLock();
1876
+ }
1379
1877
  }
1380
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1878
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1381
1879
  let rawResponseId;
1382
1880
  if (completedResponse) {
1383
1881
  rawResponseId = completedResponse.id;
@@ -1388,23 +1886,44 @@ var ResponsesAdapter = class extends AdapterBase {
1388
1886
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
1389
1887
  const auxiliaryResult = await auxiliary.finalize(factory);
1390
1888
  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));
1889
+ if (!completedEmitted) {
1890
+ completedEmitted = true;
1891
+ const finalResponse = this.buildResponse(request, {
1892
+ output,
1893
+ replay,
1894
+ stopReason,
1895
+ usage: auxiliaryResult.usage,
1896
+ billing: auxiliaryResult.billing,
1897
+ auxiliary: auxiliaryResult.auxiliary,
1898
+ warnings: auxiliaryResult.warnings,
1899
+ metadataSources: auxiliaryResult.metadataSources,
1900
+ rawResponseId
1901
+ }, factory);
1902
+ yield factory.responseCompleted({
1903
+ replay: finalResponse.replay,
1904
+ stopReason: finalResponse.stopReason,
1905
+ trace: finalResponse.backend,
1906
+ usage: finalResponse.usage,
1907
+ billing: finalResponse.billing,
1908
+ auxiliary: finalResponse.auxiliary,
1909
+ warnings: finalResponse.warnings
1910
+ });
1911
+ }
1402
1912
  }
1403
1913
  inferStopReason(response) {
1914
+ if (response.status === "failed") return "error";
1915
+ if (response.status === "incomplete") {
1916
+ const reason = response.incomplete_details?.reason;
1917
+ if (reason === "content_filter") return "content_filter";
1918
+ if (reason === "max_output_tokens") return "max_output_tokens";
1919
+ return "max_output_tokens";
1920
+ }
1404
1921
  const output = response.output;
1405
- if (!output || output.length === 0) return "unknown";
1922
+ if (!output || output.length === 0) return response.status === "completed" ? "end_turn" : "unknown";
1406
1923
  if (output.some((item) => item.type === "function_call")) return "tool_call";
1407
- if (output[output.length - 1]?.status === "incomplete") return "max_output_tokens";
1924
+ const lastItem = output[output.length - 1];
1925
+ if (lastItem?.status === "failed") return "error";
1926
+ if (lastItem?.status === "incomplete") return "max_output_tokens";
1408
1927
  return "end_turn";
1409
1928
  }
1410
1929
  };
@@ -1421,36 +1940,41 @@ var ResponsesAdapter = class extends AdapterBase {
1421
1940
  * - 高保真 replay(含 opaque continuation)
1422
1941
  * - 能力降级 warning
1423
1942
  */
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
- };
1943
+ const profile$2 = {
1944
+ kind: "messages",
1945
+ instructionsMode: "system_message",
1946
+ supportedBlockTypes: ["text", "json"],
1947
+ reasoningBlockTypes: ["text"],
1948
+ capabilities: {
1949
+ textStreaming: "native",
1950
+ reasoningStreaming: "native",
1951
+ toolCallStreaming: "synthetic",
1952
+ replay: "opaque",
1953
+ usage: "stream",
1954
+ toolResultOutcomes: ["success", "error"]
1955
+ }
1956
+ };
1957
+ const mapper$2 = new NormalizedRequestMapper(profile$2);
1958
+ function isMessagesReplayContentBlock(value) {
1959
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
1960
+ const block = value;
1961
+ switch (block.type) {
1962
+ case "text": return typeof block.text === "string";
1963
+ case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
1964
+ case "redacted_thinking": return typeof block.data === "string";
1965
+ case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
1966
+ case "tool_result":
1967
+ if (typeof block.tool_use_id !== "string") return false;
1968
+ if (block.is_error !== void 0 && typeof block.is_error !== "boolean") return false;
1969
+ if (typeof block.content === "string") return true;
1970
+ if (!Array.isArray(block.content)) return false;
1971
+ return block.content.every(isMessagesReplayContentBlock);
1972
+ default: return false;
1973
+ }
1451
1974
  }
1452
- function rollbackTrailingAssistantMessages$2(messages) {
1453
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1975
+ function assertMessagesReplayContent(content) {
1976
+ if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
1977
+ 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
1978
  }
1455
1979
  /** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
1456
1980
  function synthesizeItemId(kind, blockIndex, responseId) {
@@ -1500,34 +2024,29 @@ function buildStreamMetadata(options) {
1500
2024
  }
1501
2025
  var MessagesAdapter = class extends AdapterBase {
1502
2026
  kind = "messages";
1503
- nativeStreaming = true;
2027
+ capabilities = profile$2.capabilities;
1504
2028
  apiKey;
1505
2029
  apiVersion;
1506
2030
  baseUrl;
1507
2031
  fetchFn;
1508
- warningAccumulator;
1509
2032
  constructor(options) {
1510
2033
  super();
1511
2034
  this.apiKey = options.apiKey;
1512
2035
  this.apiVersion = options.apiVersion ?? "2023-06-01";
1513
2036
  this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
1514
2037
  this.fetchFn = options.fetch ?? globalThis.fetch;
1515
- this.warningAccumulator = [];
1516
- }
1517
- warn(message, _code) {
1518
- this.warningAccumulator.push(message);
1519
2038
  }
1520
2039
  buildRequest(request) {
1521
2040
  const messages = [];
1522
2041
  let systemPrompt;
1523
2042
  let pendingToolResultMessage;
1524
- if (request.instructions) systemPrompt = instructionsToMessagesText(request.instructions);
2043
+ if (request.instructions) systemPrompt = mapper$2.mapInstructions(request.instructions);
1525
2044
  for (const item of request.input) {
1526
2045
  if (item.type !== "tool_result") pendingToolResultMessage = void 0;
1527
2046
  switch (item.type) {
1528
2047
  case "message": {
1529
2048
  const role = item.role === "user" ? "user" : "assistant";
1530
- const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
2049
+ const supportedContent = mapper$2.ensureTextBlocks(item.content, `input message (${item.role}) content`);
1531
2050
  if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
1532
2051
  role,
1533
2052
  content: supportedContent[0].text
@@ -1554,8 +2073,8 @@ var MessagesAdapter = class extends AdapterBase {
1554
2073
  break;
1555
2074
  }
1556
2075
  case "tool_result": {
1557
- assertMessagesToolResultOutcome(item.outcome);
1558
- const content = ensureMessagesTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
2076
+ mapper$2.assertToolResultOutcome(item.outcome);
2077
+ const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1559
2078
  const block = {
1560
2079
  type: "tool_result",
1561
2080
  tool_use_id: item.callId,
@@ -1575,7 +2094,7 @@ var MessagesAdapter = class extends AdapterBase {
1575
2094
  case "reasoning": {
1576
2095
  const block = {
1577
2096
  type: "thinking",
1578
- thinking: contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"))
2097
+ thinking: contentBlocksToText(mapper$2.ensureReasoningBlocks(item.content, "reasoning content"))
1579
2098
  };
1580
2099
  const lastMsg = messages[messages.length - 1];
1581
2100
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
@@ -1585,20 +2104,20 @@ var MessagesAdapter = class extends AdapterBase {
1585
2104
  });
1586
2105
  break;
1587
2106
  }
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
- }
2107
+ case "opaque": {
2108
+ if (item.purpose !== "replay") break;
2109
+ assertOpaqueReplayEnvelope(item.payload);
2110
+ const payload = item.payload;
2111
+ if (payload.role === "assistant" && "content" in payload) {
2112
+ assertMessagesReplayContent(payload.content);
2113
+ mapper$2.rollbackTrailingAssistantMessages(messages);
2114
+ messages.push({
2115
+ role: "assistant",
2116
+ content: payload.content
2117
+ });
1600
2118
  }
1601
2119
  break;
2120
+ }
1602
2121
  }
1603
2122
  }
1604
2123
  const body = {
@@ -1625,27 +2144,53 @@ var MessagesAdapter = class extends AdapterBase {
1625
2144
  return body;
1626
2145
  }
1627
2146
  async *runStream(providerRequest, factory, request) {
1628
- this.warningAccumulator = [];
1629
2147
  const auxiliary = this.createAuxiliaryState(request);
2148
+ let completedEmitted = false;
1630
2149
  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
- });
2150
+ let response;
2151
+ try {
2152
+ response = await this.fetchFn(`${this.baseUrl}/messages`, {
2153
+ method: "POST",
2154
+ headers: {
2155
+ "Content-Type": "application/json",
2156
+ "x-api-key": this.apiKey,
2157
+ "anthropic-version": this.apiVersion
2158
+ },
2159
+ body: JSON.stringify(providerRequest)
2160
+ });
2161
+ } catch (err) {
2162
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2163
+ }
1640
2164
  if (!response.ok) {
1641
- const errorText = await response.text().catch(() => "unknown error");
1642
- throw new Error(`Messages API error ${response.status}: ${errorText}`);
2165
+ const errorBody = await response.text().catch(() => "");
2166
+ throw providerHttpError(response.status, errorBody);
1643
2167
  }
1644
2168
  const reader = response.body?.getReader();
1645
- if (!reader) throw new Error("Response body is not readable");
2169
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2170
+ const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
2171
+ let eventType = "";
2172
+ let dataStr = "";
2173
+ for (const rawLine of frame.split("\n")) {
2174
+ const line = rawLine.trim();
2175
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
2176
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
2177
+ }
2178
+ if (!eventType) return { status: "ignored" };
2179
+ try {
2180
+ const data = JSON.parse(dataStr);
2181
+ return {
2182
+ status: "parsed",
2183
+ value: {
2184
+ type: eventType,
2185
+ data
2186
+ }
2187
+ };
2188
+ } catch {
2189
+ return { status: "malformed" };
2190
+ }
2191
+ });
1646
2192
  const output = [];
1647
- const decoder = new TextDecoder();
1648
- let buffer = "";
2193
+ let streamDone = false;
1649
2194
  let messageResponse;
1650
2195
  let currentContentBlockIndex = -1;
1651
2196
  let currentItemType = null;
@@ -1667,11 +2212,10 @@ var MessagesAdapter = class extends AdapterBase {
1667
2212
  }
1668
2213
  try {
1669
2214
  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;
2215
+ const { done, value } = await reader.read().catch((err) => {
2216
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2217
+ });
2218
+ const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
1675
2219
  const malformedWarning = emitMalformedStreamWarning(factory, {
1676
2220
  count: malformedEvents,
1677
2221
  providerLabel: "Messages",
@@ -1683,7 +2227,6 @@ var MessagesAdapter = class extends AdapterBase {
1683
2227
  case "error": {
1684
2228
  const err = sseEvent.data.error;
1685
2229
  yield factory.responseWarning(err.message, err.type);
1686
- this.warn(err.message, err.type);
1687
2230
  continue;
1688
2231
  }
1689
2232
  case "message_start":
@@ -1718,7 +2261,7 @@ var MessagesAdapter = class extends AdapterBase {
1718
2261
  yield factory.reasoningStarted(currentItemId, "redacted");
1719
2262
  yield factory.reasoningDelta(currentItemId, textBlock(data));
1720
2263
  const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
1721
- yield factory.reasoningCompleted(redactedItem);
2264
+ yield factory.reasoningCompleted(currentItemId);
1722
2265
  output.push(redactedItem);
1723
2266
  rawReplayContent.push({
1724
2267
  type: "redacted_thinking",
@@ -1747,7 +2290,7 @@ var MessagesAdapter = class extends AdapterBase {
1747
2290
  if (currentItemType === "message" && currentItemId) {
1748
2291
  const txt = delta.text;
1749
2292
  textBuffer += txt;
1750
- yield factory.messageDelta(currentItemId, txt);
2293
+ yield factory.messageDelta(currentItemId, textBlock(txt));
1751
2294
  }
1752
2295
  break;
1753
2296
  case "thinking_delta":
@@ -1769,14 +2312,14 @@ var MessagesAdapter = class extends AdapterBase {
1769
2312
  }
1770
2313
  case "content_block_stop":
1771
2314
  if (currentItemType === "message" && currentItemId) {
1772
- yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
2315
+ yield factory.messageCompleted(currentItemId);
1773
2316
  output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
1774
2317
  rawReplayContent.push({
1775
2318
  type: "text",
1776
2319
  text: textBuffer
1777
2320
  });
1778
2321
  } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
1779
- yield factory.reasoningCompleted(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
2322
+ yield factory.reasoningCompleted(currentItemId);
1780
2323
  output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
1781
2324
  rawReplayContent.push({
1782
2325
  type: "thinking",
@@ -1784,7 +2327,7 @@ var MessagesAdapter = class extends AdapterBase {
1784
2327
  });
1785
2328
  } else if (currentItemType === "tool_call" && currentItemId) {
1786
2329
  const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
1787
- yield factory.toolCallCompleted(tcItem);
2330
+ yield factory.toolCallCompleted(currentItemId);
1788
2331
  output.push(tcItem);
1789
2332
  rawReplayContent.push({
1790
2333
  type: "tool_use",
@@ -1805,11 +2348,19 @@ var MessagesAdapter = class extends AdapterBase {
1805
2348
  }
1806
2349
  case "message_stop": break;
1807
2350
  }
2351
+ if (done) {
2352
+ streamDone = true;
2353
+ break;
2354
+ }
1808
2355
  }
1809
2356
  } finally {
1810
- reader.releaseLock();
2357
+ try {
2358
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2359
+ } finally {
2360
+ reader.releaseLock();
2361
+ }
1811
2362
  }
1812
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
2363
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
1813
2364
  const replay = [...replayFromOutput(output)];
1814
2365
  if (messageResponse) {
1815
2366
  const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
@@ -1830,17 +2381,29 @@ var MessagesAdapter = class extends AdapterBase {
1830
2381
  if (!hasStreamedReasoning) {}
1831
2382
  const auxiliaryResult = await auxiliary.finalize(factory);
1832
2383
  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));
2384
+ if (!completedEmitted) {
2385
+ completedEmitted = true;
2386
+ const finalResponse = this.buildResponse(request, {
2387
+ output,
2388
+ replay,
2389
+ stopReason: stopReason ? mapStopReason(stopReason) : void 0,
2390
+ usage: auxiliaryResult.usage,
2391
+ billing: auxiliaryResult.billing,
2392
+ auxiliary: auxiliaryResult.auxiliary,
2393
+ warnings: auxiliaryResult.warnings,
2394
+ metadataSources: auxiliaryResult.metadataSources,
2395
+ rawResponseId
2396
+ }, factory);
2397
+ yield factory.responseCompleted({
2398
+ replay: finalResponse.replay,
2399
+ stopReason: finalResponse.stopReason,
2400
+ trace: finalResponse.backend,
2401
+ usage: finalResponse.usage,
2402
+ billing: finalResponse.billing,
2403
+ auxiliary: finalResponse.auxiliary,
2404
+ warnings: finalResponse.warnings
2405
+ });
2406
+ }
1844
2407
  }
1845
2408
  };
1846
2409
  //#endregion
@@ -1855,51 +2418,21 @@ var MessagesAdapter = class extends AdapterBase {
1855
2418
  * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
1856
2419
  */
1857
2420
  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
- }
2421
+ const profile$1 = {
2422
+ kind: "chat-completions",
2423
+ instructionsMode: "system_message",
2424
+ supportedBlockTypes: ["text", "json"],
2425
+ reasoningBlockTypes: ["text"],
2426
+ capabilities: {
2427
+ textStreaming: "native",
2428
+ reasoningStreaming: "native",
2429
+ toolCallStreaming: "native",
2430
+ replay: "opaque",
2431
+ usage: "final",
2432
+ toolResultOutcomes: ["success"]
1885
2433
  }
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");
1897
- }
1898
- return blocks;
1899
- }
1900
- function contentBlocksToChatText(blocks, field) {
1901
- return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
1902
- }
2434
+ };
2435
+ const mapper$1 = new NormalizedRequestMapper(profile$1);
1903
2436
  function extractReasoningText(value) {
1904
2437
  if (typeof value === "string") return value;
1905
2438
  if (Array.isArray(value)) return value.map(extractReasoningText).join("");
@@ -1930,8 +2463,31 @@ function extractReasoningDeltas(delta) {
1930
2463
  }
1931
2464
  return deltas;
1932
2465
  }
1933
- function rollbackTrailingAssistantMessages$1(messages) {
1934
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
2466
+ function isChatReplayToolCall(value) {
2467
+ if (!value || typeof value !== "object") return false;
2468
+ const entry = value;
2469
+ if (typeof entry.id !== "string" || entry.type !== "function") return false;
2470
+ const fn = entry.function;
2471
+ if (!fn || typeof fn !== "object") return false;
2472
+ const f = fn;
2473
+ return typeof f.name === "string" && typeof f.arguments === "string";
2474
+ }
2475
+ function isChatReplayMessage(value) {
2476
+ if (!value || typeof value !== "object") return false;
2477
+ const msg = value;
2478
+ const role = msg.role;
2479
+ if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
2480
+ if (!(msg.content === null || typeof msg.content === "string")) return false;
2481
+ if (msg.tool_calls !== void 0) {
2482
+ if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
2483
+ }
2484
+ if (msg.tool_call_id !== void 0 && typeof msg.tool_call_id !== "string") return false;
2485
+ if (msg.name !== void 0 && typeof msg.name !== "string") return false;
2486
+ return true;
2487
+ }
2488
+ function assertChatReplayMessages(messages, field) {
2489
+ if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
2490
+ 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
2491
  }
1936
2492
  function buildAssistantReplayMessage(params) {
1937
2493
  const { content, reasoningByField, toolCalls } = params;
@@ -1953,7 +2509,7 @@ function buildAssistantReplayMessage(params) {
1953
2509
  }
1954
2510
  var ChatCompletionsAdapter = class extends AdapterBase {
1955
2511
  kind = "chat-completions";
1956
- nativeStreaming = true;
2512
+ capabilities = profile$1.capabilities;
1957
2513
  apiKey;
1958
2514
  baseUrl;
1959
2515
  fetchFn;
@@ -1965,17 +2521,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
1965
2521
  }
1966
2522
  buildRequest(request) {
1967
2523
  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
- }
2524
+ if (request.instructions) messages.push({
2525
+ role: "system",
2526
+ content: mapper$1.mapInstructions(request.instructions)
2527
+ });
1975
2528
  for (const item of request.input) switch (item.type) {
1976
2529
  case "message": {
1977
2530
  const role = item.role;
1978
- const text = contentBlocksToChatText(item.content, `input message (${item.role}) content`);
2531
+ const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
1979
2532
  messages.push({
1980
2533
  role,
1981
2534
  content: text || null
@@ -2001,38 +2554,44 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2001
2554
  break;
2002
2555
  }
2003
2556
  case "tool_result":
2004
- assertChatToolResultOutcome(item.outcome);
2557
+ mapper$1.assertToolResultOutcome(item.outcome);
2005
2558
  messages.push({
2006
2559
  role: "tool",
2007
2560
  tool_call_id: item.callId,
2008
2561
  name: item.toolName,
2009
- content: contentBlocksToChatText(item.content, `tool_result ${item.callId} content`)
2562
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2010
2563
  });
2011
2564
  break;
2012
2565
  case "reasoning":
2013
2566
  messages.push({
2014
2567
  role: "assistant",
2015
- content: contentBlocksToChatText(item.content, "reasoning content")
2568
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
2016
2569
  });
2017
2570
  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);
2571
+ case "opaque": {
2572
+ if (item.purpose !== "replay") break;
2573
+ assertOpaqueReplayEnvelope(item.payload);
2574
+ const payload = item.payload;
2575
+ if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
2576
+ role: "assistant",
2577
+ content: payload.content
2578
+ });
2579
+ else if (payload.replaceCanonical === true && "messages" in payload) {
2580
+ assertChatReplayMessages(payload.messages, "messages");
2581
+ mapper$1.rollbackTrailingAssistantMessages(messages);
2582
+ for (const m of payload.messages) messages.push(m);
2583
+ } else if ("messages" in payload) {
2584
+ assertChatReplayMessages(payload.messages, "messages");
2585
+ for (const m of payload.messages) messages.push(m);
2029
2586
  }
2030
2587
  break;
2588
+ }
2031
2589
  }
2032
2590
  const body = {
2033
2591
  model: request.model,
2034
2592
  messages,
2035
- stream: true
2593
+ stream: true,
2594
+ n: 1
2036
2595
  };
2037
2596
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2038
2597
  type: "function",
@@ -2057,23 +2616,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2057
2616
  }
2058
2617
  async *runStream(providerRequest, factory, request) {
2059
2618
  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
- });
2619
+ let response;
2620
+ try {
2621
+ response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2622
+ method: "POST",
2623
+ headers: {
2624
+ "Content-Type": "application/json",
2625
+ Authorization: `Bearer ${this.apiKey}`
2626
+ },
2627
+ body: JSON.stringify(providerRequest)
2628
+ });
2629
+ } catch (err) {
2630
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2631
+ }
2068
2632
  if (!response.ok) {
2069
- const errorText = await response.text().catch(() => "unknown error");
2070
- throw new Error(`Chat Completions API error ${response.status}: ${errorText}`);
2633
+ const errorBody = await response.text().catch(() => "");
2634
+ throw providerHttpError(response.status, errorBody);
2071
2635
  }
2072
2636
  const reader = response.body?.getReader();
2073
- if (!reader) throw new Error("Response body is not readable");
2637
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2638
+ const parser = new IncrementalStreamParser(splitLines, (item) => {
2639
+ const trimmed = item.trim();
2640
+ if (!trimmed.startsWith("data: ")) return { status: "ignored" };
2641
+ const data = trimmed.slice(6).trim();
2642
+ if (data === "[DONE]") return { status: "ignored" };
2643
+ try {
2644
+ return {
2645
+ status: "parsed",
2646
+ value: JSON.parse(data)
2647
+ };
2648
+ } catch {
2649
+ return { status: "malformed" };
2650
+ }
2651
+ });
2074
2652
  const output = [];
2075
- const decoder = new TextDecoder();
2076
- let buffer = "";
2653
+ let streamDone = false;
2077
2654
  let responseId;
2078
2655
  let accumulatedContent = "";
2079
2656
  let accumulatedReasoning = "";
@@ -2081,6 +2658,9 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2081
2658
  let currentReasoningId = "";
2082
2659
  let hasMessageStarted = false;
2083
2660
  let hasReasoningStarted = false;
2661
+ let completedEmitted = false;
2662
+ let warnedNonZeroChoice = false;
2663
+ const buildResponse = this.buildResponse.bind(this);
2084
2664
  const pendingToolCalls = /* @__PURE__ */ new Map();
2085
2665
  const reasoningByField = /* @__PURE__ */ new Map();
2086
2666
  const finalizePendingTurn = () => {
@@ -2089,17 +2669,17 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2089
2669
  const finalizedReasoningByField = new Map(reasoningByField);
2090
2670
  if (hasReasoningStarted && accumulatedReasoning) {
2091
2671
  const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
2092
- events.push(factory.reasoningCompleted(reasoning));
2672
+ events.push(factory.reasoningCompleted(currentReasoningId));
2093
2673
  output.push(reasoning);
2094
2674
  }
2095
- if (hasMessageStarted && accumulatedContent) {
2675
+ if (hasMessageStarted) {
2096
2676
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2097
- events.push(factory.messageCompleted(message));
2098
- output.push(message);
2677
+ events.push(factory.messageCompleted(currentMessageId));
2678
+ if (accumulatedContent) output.push(message);
2099
2679
  }
2100
2680
  for (const pending of finalizedToolCalls) {
2101
2681
  const toolCall = toolCallItem(pending.id, pending.name, pending.args);
2102
- events.push(factory.toolCallCompleted(toolCall));
2682
+ events.push(factory.toolCallCompleted(pending.id));
2103
2683
  output.push(toolCall);
2104
2684
  }
2105
2685
  const assistantReplayMessage = buildAssistantReplayMessage({
@@ -2120,13 +2700,46 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2120
2700
  assistantReplayMessage
2121
2701
  };
2122
2702
  };
2703
+ const emitCompleted = async function* (stopReason, assistantReplayMessage, rawResponseId) {
2704
+ if (completedEmitted) {
2705
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2706
+ return;
2707
+ }
2708
+ completedEmitted = true;
2709
+ const replay = [...replayFromOutput(output)];
2710
+ if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2711
+ replaceCanonical: true,
2712
+ messages: [assistantReplayMessage]
2713
+ }));
2714
+ const auxiliaryResult = await auxiliary.finalize(factory);
2715
+ for (const event of auxiliaryResult.events) yield event;
2716
+ const finalResponse = buildResponse(request, {
2717
+ output,
2718
+ replay,
2719
+ stopReason,
2720
+ usage: auxiliaryResult.usage,
2721
+ billing: auxiliaryResult.billing,
2722
+ auxiliary: auxiliaryResult.auxiliary,
2723
+ warnings: auxiliaryResult.warnings,
2724
+ metadataSources: auxiliaryResult.metadataSources,
2725
+ rawResponseId
2726
+ }, factory);
2727
+ yield factory.responseCompleted({
2728
+ replay: finalResponse.replay,
2729
+ stopReason: finalResponse.stopReason,
2730
+ trace: finalResponse.backend,
2731
+ usage: finalResponse.usage,
2732
+ billing: finalResponse.billing,
2733
+ auxiliary: finalResponse.auxiliary,
2734
+ warnings: finalResponse.warnings
2735
+ });
2736
+ };
2123
2737
  try {
2124
2738
  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;
2739
+ const { done, value } = await reader.read().catch((err) => {
2740
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2741
+ });
2742
+ const { items: chunks, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
2130
2743
  const malformedWarning = emitMalformedStreamWarning(factory, {
2131
2744
  count: malformedEvents,
2132
2745
  providerLabel: "Chat Completions",
@@ -2137,14 +2750,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2137
2750
  responseId = chunk.id;
2138
2751
  if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
2139
2752
  for (const choice of chunk.choices) {
2140
- if (choice.index !== 0) continue;
2753
+ if (choice.index !== 0) {
2754
+ if (!warnedNonZeroChoice) {
2755
+ 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");
2756
+ warnedNonZeroChoice = true;
2757
+ }
2758
+ continue;
2759
+ }
2760
+ if (completedEmitted) {
2761
+ if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2762
+ continue;
2763
+ }
2141
2764
  const delta = choice.delta;
2142
2765
  const finishReason = choice.finish_reason;
2143
2766
  const reasoningDeltas = extractReasoningDeltas(delta);
2144
- if (delta.role === "assistant" && typeof delta.content === "string" && !hasMessageStarted) {
2767
+ const ensureMessageStarted = () => {
2768
+ if (hasMessageStarted) return;
2145
2769
  currentMessageId = `msg-${chunk.id}`;
2146
2770
  hasMessageStarted = true;
2147
2771
  accumulatedContent = "";
2772
+ };
2773
+ if (delta.role === "assistant" && !hasMessageStarted) {
2774
+ ensureMessageStarted();
2148
2775
  yield factory.messageStarted(currentMessageId);
2149
2776
  }
2150
2777
  if (reasoningDeltas.length > 0) {
@@ -2162,32 +2789,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2162
2789
  }
2163
2790
  if (delta.content) {
2164
2791
  if (!hasMessageStarted) {
2165
- currentMessageId = `msg-${chunk.id}`;
2166
- hasMessageStarted = true;
2792
+ ensureMessageStarted();
2167
2793
  yield factory.messageStarted(currentMessageId);
2168
2794
  }
2169
2795
  accumulatedContent += delta.content;
2170
- yield factory.messageDelta(currentMessageId, delta.content);
2796
+ yield factory.messageDelta(currentMessageId, textBlock(delta.content));
2171
2797
  }
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 ?? "");
2798
+ if (delta.tool_calls) {
2799
+ if (!hasMessageStarted) {
2800
+ ensureMessageStarted();
2801
+ yield factory.messageStarted(currentMessageId);
2181
2802
  }
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 });
2803
+ for (const tc of delta.tool_calls) {
2804
+ const idx = tc.index;
2805
+ if (tc.id) {
2806
+ pendingToolCalls.set(idx, {
2807
+ id: tc.id,
2808
+ name: tc.function?.name ?? "",
2809
+ args: ""
2810
+ });
2811
+ yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2812
+ }
2813
+ if (tc.function?.arguments) {
2814
+ const pending = pendingToolCalls.get(idx);
2815
+ if (pending) {
2816
+ pending.args += tc.function.arguments;
2817
+ yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2818
+ }
2187
2819
  }
2188
2820
  }
2189
2821
  }
2190
2822
  if (delta.function_call) {
2823
+ if (!hasMessageStarted) {
2824
+ ensureMessageStarted();
2825
+ yield factory.messageStarted(currentMessageId);
2826
+ }
2191
2827
  if (delta.function_call.name) {
2192
2828
  const fcId = `fc-${chunk.id}-0`;
2193
2829
  pendingToolCalls.set(0, {
@@ -2208,54 +2844,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2208
2844
  if (finishReason && finishReason !== null) {
2209
2845
  const { events, assistantReplayMessage } = finalizePendingTurn();
2210
2846
  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));
2847
+ yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
2230
2848
  }
2231
2849
  }
2232
2850
  }
2851
+ if (done) {
2852
+ streamDone = true;
2853
+ break;
2854
+ }
2233
2855
  }
2234
2856
  } finally {
2235
- reader.releaseLock();
2857
+ try {
2858
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2859
+ } finally {
2860
+ reader.releaseLock();
2861
+ }
2236
2862
  }
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) {
2863
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2864
+ if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
2239
2865
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2240
2866
  const { events, assistantReplayMessage } = finalizePendingTurn();
2241
2867
  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));
2868
+ yield* emitCompleted(void 0, assistantReplayMessage, responseId);
2259
2869
  }
2260
2870
  }
2261
2871
  };
@@ -2277,23 +2887,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2277
2887
  * - tool_call 不支持逐 token 流式
2278
2888
  * - replay 保真度低(无 opaque continuation 机制)
2279
2889
  */
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
- }
2890
+ const profile = {
2891
+ kind: "ollama",
2892
+ instructionsMode: "system_message",
2893
+ supportedBlockTypes: ["text", "json"],
2894
+ reasoningBlockTypes: ["text"],
2895
+ capabilities: {
2896
+ textStreaming: "native",
2897
+ reasoningStreaming: "none",
2898
+ toolCallStreaming: "synthetic",
2899
+ replay: "opaque",
2900
+ usage: "final",
2901
+ toolResultOutcomes: ["success"]
2902
+ }
2903
+ };
2904
+ const mapper = new NormalizedRequestMapper(profile);
2297
2905
  function parseOllamaToolArguments(item) {
2298
2906
  if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2299
2907
  try {
@@ -2302,46 +2910,24 @@ function parseOllamaToolArguments(item) {
2302
2910
  } catch {}
2303
2911
  throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2304
2912
  }
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) {
2913
+ function isOllamaReplayToolCalls(value) {
2336
2914
  return Array.isArray(value) && value.every((entry) => {
2337
2915
  if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
2338
2916
  const fn = entry.function;
2917
+ const id = entry.id;
2918
+ if (id !== void 0 && typeof id !== "string") return false;
2339
2919
  return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
2340
2920
  });
2341
2921
  }
2922
+ function toWireOllamaToolCalls(toolCalls) {
2923
+ return toolCalls.map((tc) => ({ function: {
2924
+ name: tc.function.name,
2925
+ arguments: tc.function.arguments
2926
+ } }));
2927
+ }
2342
2928
  var OllamaAdapter = class extends AdapterBase {
2343
2929
  kind = "ollama";
2344
- nativeStreaming = true;
2930
+ capabilities = profile.capabilities;
2345
2931
  baseUrl;
2346
2932
  apiKey;
2347
2933
  fetchFn;
@@ -2354,16 +2940,18 @@ var OllamaAdapter = class extends AdapterBase {
2354
2940
  buildRequest(request) {
2355
2941
  if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2356
2942
  const messages = [];
2943
+ /** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
2944
+ const callIdsByName = /* @__PURE__ */ new Map();
2357
2945
  if (request.instructions) messages.push({
2358
2946
  role: "system",
2359
- content: instructionsToOllamaText(request.instructions)
2947
+ content: mapper.mapInstructions(request.instructions)
2360
2948
  });
2361
2949
  for (const item of request.input) switch (item.type) {
2362
2950
  case "message": {
2363
2951
  const role = item.role;
2364
2952
  messages.push({
2365
2953
  role,
2366
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`))
2954
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
2367
2955
  });
2368
2956
  break;
2369
2957
  }
@@ -2373,6 +2961,9 @@ var OllamaAdapter = class extends AdapterBase {
2373
2961
  name: item.name,
2374
2962
  arguments: parseOllamaToolArguments(item)
2375
2963
  } };
2964
+ const queue = callIdsByName.get(item.name) ?? [];
2965
+ queue.push(item.id);
2966
+ callIdsByName.set(item.name, queue);
2376
2967
  if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
2377
2968
  else messages.push({
2378
2969
  role: "assistant",
@@ -2381,32 +2972,48 @@ var OllamaAdapter = class extends AdapterBase {
2381
2972
  });
2382
2973
  break;
2383
2974
  }
2384
- case "tool_result":
2385
- assertOllamaToolResultOutcome(item.outcome);
2975
+ case "tool_result": {
2976
+ mapper.assertToolResultOutcome(item.outcome);
2977
+ const queue = callIdsByName.get(item.toolName);
2978
+ if (queue && queue.length > 0) queue.shift();
2386
2979
  messages.push({
2387
2980
  role: "tool",
2388
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `tool_result ${item.callId} content`))
2981
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2389
2982
  });
2390
2983
  break;
2984
+ }
2391
2985
  case "reasoning":
2392
2986
  messages.push({
2393
2987
  role: "assistant",
2394
- content: contentBlocksToText(ensureOllamaReasoningBlocks(item.content, "reasoning content"))
2988
+ content: contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"))
2395
2989
  });
2396
2990
  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
- });
2991
+ case "opaque": {
2992
+ if (item.source !== "ollama" || item.purpose !== "replay") break;
2993
+ assertOpaqueReplayEnvelope(item.payload);
2994
+ const payload = item.payload;
2995
+ if (payload.role === "assistant" && typeof payload.content === "string") {
2996
+ mapper.rollbackTrailingAssistantMessages(messages);
2997
+ let replayToolCalls;
2998
+ if ("tool_calls" in payload && payload.tool_calls !== void 0) {
2999
+ 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");
3000
+ replayToolCalls = payload.tool_calls;
2407
3001
  }
3002
+ if (replayToolCalls) {
3003
+ for (const tc of replayToolCalls) if (tc.id) {
3004
+ const queue = callIdsByName.get(tc.function.name) ?? [];
3005
+ queue.push(tc.id);
3006
+ callIdsByName.set(tc.function.name, queue);
3007
+ }
3008
+ }
3009
+ messages.push({
3010
+ role: "assistant",
3011
+ content: payload.content,
3012
+ tool_calls: replayToolCalls ? toWireOllamaToolCalls(replayToolCalls) : void 0
3013
+ });
2408
3014
  }
2409
3015
  break;
3016
+ }
2410
3017
  }
2411
3018
  const body = {
2412
3019
  model: request.model,
@@ -2430,35 +3037,96 @@ var OllamaAdapter = class extends AdapterBase {
2430
3037
  }
2431
3038
  async *runStream(providerRequest, factory, request) {
2432
3039
  const auxiliary = this.createAuxiliaryState(request);
3040
+ let completedEmitted = false;
2433
3041
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
2434
3042
  const headers = { "Content-Type": "application/json" };
2435
3043
  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
- });
3044
+ let response;
3045
+ try {
3046
+ response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3047
+ method: "POST",
3048
+ headers,
3049
+ body: JSON.stringify(providerRequest)
3050
+ });
3051
+ } catch (err) {
3052
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
3053
+ }
2441
3054
  if (!response.ok) {
2442
- const errorText = await response.text().catch(() => "unknown error");
2443
- throw new Error(`Ollama API error ${response.status}: ${errorText}`);
3055
+ const errorBody = await response.text().catch(() => "");
3056
+ throw providerHttpError(response.status, errorBody);
2444
3057
  }
2445
3058
  const reader = response.body?.getReader();
2446
- if (!reader) throw new Error("Response body is not readable");
3059
+ if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
3060
+ const parser = new IncrementalStreamParser(splitLines, (item) => {
3061
+ const trimmed = item.trim();
3062
+ if (!trimmed) return { status: "ignored" };
3063
+ try {
3064
+ const parsed = JSON.parse(trimmed);
3065
+ if (parsed && typeof parsed === "object" && "message" in parsed) return {
3066
+ status: "parsed",
3067
+ value: parsed
3068
+ };
3069
+ return { status: "malformed" };
3070
+ } catch {
3071
+ return { status: "malformed" };
3072
+ }
3073
+ });
2447
3074
  const output = [];
2448
- const decoder = new TextDecoder();
2449
- let buffer = "";
3075
+ let streamDone = false;
2450
3076
  let responseId;
2451
3077
  let accumulatedContent = "";
2452
3078
  let currentMessageId = "";
2453
3079
  let hasMessageStarted = false;
2454
3080
  let pendingToolCalls = [];
3081
+ let toolCallIndex = 0;
3082
+ const buildResponse = this.buildResponse.bind(this);
3083
+ const emitCompleted = async function* (stopReason, rawResponseId) {
3084
+ if (completedEmitted) {
3085
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3086
+ return;
3087
+ }
3088
+ completedEmitted = true;
3089
+ const replay = replayFromOutput(output);
3090
+ if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
3091
+ role: "assistant",
3092
+ content: accumulatedContent,
3093
+ tool_calls: pendingToolCalls.map((tc) => ({
3094
+ id: tc.id,
3095
+ function: {
3096
+ name: tc.name,
3097
+ arguments: tc.argumentsJson
3098
+ }
3099
+ }))
3100
+ }));
3101
+ const auxiliaryResult = await auxiliary.finalize(factory);
3102
+ for (const event of auxiliaryResult.events) yield event;
3103
+ const finalResponse = buildResponse(request, {
3104
+ output,
3105
+ replay,
3106
+ stopReason,
3107
+ usage: auxiliaryResult.usage,
3108
+ billing: auxiliaryResult.billing,
3109
+ auxiliary: auxiliaryResult.auxiliary,
3110
+ warnings: auxiliaryResult.warnings,
3111
+ metadataSources: auxiliaryResult.metadataSources,
3112
+ rawResponseId
3113
+ }, factory);
3114
+ yield factory.responseCompleted({
3115
+ replay: finalResponse.replay,
3116
+ stopReason: finalResponse.stopReason,
3117
+ trace: finalResponse.backend,
3118
+ usage: finalResponse.usage,
3119
+ billing: finalResponse.billing,
3120
+ auxiliary: finalResponse.auxiliary,
3121
+ warnings: finalResponse.warnings
3122
+ });
3123
+ };
2455
3124
  try {
2456
3125
  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;
3126
+ const { done, value } = await reader.read().catch((err) => {
3127
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
3128
+ });
3129
+ const { items: chunks, malformed: malformedLines } = done ? parser.flush() : parser.feed(value);
2462
3130
  const malformedWarning = emitMalformedStreamWarning(factory, {
2463
3131
  count: malformedLines,
2464
3132
  providerLabel: "Ollama",
@@ -2467,6 +3135,10 @@ var OllamaAdapter = class extends AdapterBase {
2467
3135
  if (malformedWarning) yield malformedWarning;
2468
3136
  for (const chunk of chunks) {
2469
3137
  responseId = chunk.created_at;
3138
+ if (completedEmitted) {
3139
+ if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3140
+ continue;
3141
+ }
2470
3142
  const msg = chunk.message;
2471
3143
  if (msg.content) {
2472
3144
  if (!hasMessageStarted) {
@@ -2475,10 +3147,10 @@ var OllamaAdapter = class extends AdapterBase {
2475
3147
  yield factory.messageStarted(currentMessageId);
2476
3148
  }
2477
3149
  accumulatedContent += msg.content;
2478
- yield factory.messageDelta(currentMessageId, msg.content);
3150
+ yield factory.messageDelta(currentMessageId, textBlock(msg.content));
2479
3151
  }
2480
3152
  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}`;
3153
+ const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
2482
3154
  const argsText = JSON.stringify(tc.function.arguments);
2483
3155
  pendingToolCalls.push({
2484
3156
  id: tcId,
@@ -2495,14 +3167,15 @@ var OllamaAdapter = class extends AdapterBase {
2495
3167
  }
2496
3168
  if (hasMessageStarted) {
2497
3169
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2498
- yield factory.messageCompleted(message);
3170
+ yield factory.messageCompleted(currentMessageId);
2499
3171
  if (accumulatedContent) output.push(message);
2500
3172
  }
3173
+ 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
3174
  for (const pending of pendingToolCalls) {
2502
3175
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2503
3176
  yield factory.toolCallStarted(pending.id, pending.name);
2504
3177
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2505
- yield factory.toolCallCompleted(toolCall);
3178
+ yield factory.toolCallCompleted(pending.id);
2506
3179
  output.push(toolCall);
2507
3180
  }
2508
3181
  if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
@@ -2512,67 +3185,42 @@ var OllamaAdapter = class extends AdapterBase {
2512
3185
  prompt_eval_count: chunk.prompt_eval_count,
2513
3186
  eval_count: chunk.eval_count
2514
3187
  });
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));
3188
+ yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
2538
3189
  accumulatedContent = "";
2539
3190
  currentMessageId = "";
2540
3191
  hasMessageStarted = false;
2541
3192
  pendingToolCalls = [];
2542
3193
  }
2543
3194
  }
3195
+ if (done) {
3196
+ streamDone = true;
3197
+ break;
3198
+ }
2544
3199
  }
2545
3200
  } finally {
2546
- reader.releaseLock();
3201
+ try {
3202
+ if (!streamDone) await reader.cancel().catch(() => void 0);
3203
+ } finally {
3204
+ reader.releaseLock();
3205
+ }
2547
3206
  }
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) {
3207
+ if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
3208
+ if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
2550
3209
  yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
2551
3210
  if (hasMessageStarted) {
2552
3211
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2553
- yield factory.messageCompleted(message);
3212
+ yield factory.messageCompleted(currentMessageId);
2554
3213
  if (accumulatedContent) output.push(message);
2555
3214
  }
3215
+ 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
3216
  for (const pending of pendingToolCalls) {
2557
3217
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2558
3218
  yield factory.toolCallStarted(pending.id, pending.name);
2559
3219
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2560
- yield factory.toolCallCompleted(toolCall);
3220
+ yield factory.toolCallCompleted(pending.id);
2561
3221
  output.push(toolCall);
2562
3222
  }
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));
3223
+ yield* emitCompleted(void 0, responseId);
2576
3224
  }
2577
3225
  }
2578
3226
  };
@@ -2607,7 +3255,18 @@ function assertMockRequest(request, expectation, context) {
2607
3255
  }
2608
3256
  var MockAdapter = class extends AdapterBase {
2609
3257
  kind = "mock";
2610
- nativeStreaming = false;
3258
+ capabilities = {
3259
+ textStreaming: "synthetic",
3260
+ reasoningStreaming: "synthetic",
3261
+ toolCallStreaming: "synthetic",
3262
+ replay: "canonical",
3263
+ usage: "final",
3264
+ toolResultOutcomes: [
3265
+ "success",
3266
+ "error",
3267
+ "rejected"
3268
+ ]
3269
+ };
2611
3270
  handler;
2612
3271
  providerMetadata;
2613
3272
  cursor = 0;
@@ -2679,18 +3338,34 @@ var MockAdapter = class extends AdapterBase {
2679
3338
  break;
2680
3339
  }
2681
3340
  case "complete": {
2682
- const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
2683
- yield factory.responseCompleted(response);
3341
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
3342
+ yield factory.responseCompleted({
3343
+ replay: finalResponse.replay,
3344
+ stopReason: finalResponse.stopReason,
3345
+ trace: finalResponse.backend,
3346
+ usage: finalResponse.usage,
3347
+ billing: finalResponse.billing,
3348
+ auxiliary: finalResponse.auxiliary,
3349
+ warnings: finalResponse.warnings
3350
+ });
2684
3351
  return;
2685
3352
  }
2686
3353
  case "error": {
2687
3354
  yield factory.responseWarning(step.message, step.code);
2688
- const response = this.finalizeTurn(request, factory, mockRequest, output, {
3355
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, {
2689
3356
  type: "complete",
2690
3357
  stopReason: step.stopReason ?? "error",
2691
3358
  providerMetadata: step.providerMetadata
2692
3359
  }, stepCount);
2693
- yield factory.responseCompleted(response);
3360
+ yield factory.responseCompleted({
3361
+ replay: finalResponse.replay,
3362
+ stopReason: finalResponse.stopReason,
3363
+ trace: finalResponse.backend,
3364
+ usage: finalResponse.usage,
3365
+ billing: finalResponse.billing,
3366
+ auxiliary: finalResponse.auxiliary,
3367
+ warnings: finalResponse.warnings
3368
+ });
2694
3369
  return;
2695
3370
  }
2696
3371
  case "interrupt":
@@ -2699,8 +3374,16 @@ var MockAdapter = class extends AdapterBase {
2699
3374
  case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2700
3375
  }
2701
3376
  }
2702
- const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
2703
- yield factory.responseCompleted(response);
3377
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
3378
+ yield factory.responseCompleted({
3379
+ replay: finalResponse.replay,
3380
+ stopReason: finalResponse.stopReason,
3381
+ trace: finalResponse.backend,
3382
+ usage: finalResponse.usage,
3383
+ billing: finalResponse.billing,
3384
+ auxiliary: finalResponse.auxiliary,
3385
+ warnings: finalResponse.warnings
3386
+ });
2704
3387
  } finally {
2705
3388
  this.activeStream = false;
2706
3389
  }
@@ -2828,10 +3511,11 @@ async function* emitMessage(factory, item, stream) {
2828
3511
  let chunkIndex = 0;
2829
3512
  for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
2830
3513
  await delayForChunk(stream, chunkIndex, chunk.length);
2831
- yield factory.messageDelta(item.id, chunk);
3514
+ yield factory.messageDelta(item.id, textBlock(chunk));
2832
3515
  chunkIndex += 1;
2833
3516
  }
2834
- yield factory.messageCompleted(item);
3517
+ else yield factory.messageDelta(item.id, block);
3518
+ yield factory.messageCompleted(item.id);
2835
3519
  }
2836
3520
  async function* emitReasoning(factory, item, stream) {
2837
3521
  if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
@@ -2848,7 +3532,7 @@ async function* emitReasoning(factory, item, stream) {
2848
3532
  chunkIndex += 1;
2849
3533
  }
2850
3534
  }
2851
- yield factory.reasoningCompleted(item);
3535
+ yield factory.reasoningCompleted(item.id);
2852
3536
  }
2853
3537
  async function* emitToolCall(factory, item, streamArguments, stream) {
2854
3538
  yield factory.toolCallStarted(item.id, item.name);
@@ -2860,7 +3544,7 @@ async function* emitToolCall(factory, item, streamArguments, stream) {
2860
3544
  chunkIndex += 1;
2861
3545
  }
2862
3546
  }
2863
- yield factory.toolCallCompleted(item);
3547
+ yield factory.toolCallCompleted(item.id);
2864
3548
  }
2865
3549
  function resolveStepStreamOptions(defaults, override, label) {
2866
3550
  if (override === false) return;
@@ -2971,108 +3655,6 @@ function cloneItem(item) {
2971
3655
  return structuredClone(item);
2972
3656
  }
2973
3657
  //#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 };
3658
+ 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
3659
 
3078
3660
  //# sourceMappingURL=index.mjs.map