@codehz/ai 0.1.8 → 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;
@@ -1020,16 +1230,6 @@ function record(obj) {
1020
1230
  for (const [key, value] of Object.entries(obj)) if (value !== void 0) out[key] = value;
1021
1231
  return out;
1022
1232
  }
1023
- function billableFromOpenAIStyle(inputTokens, outputTokens, cachedInputTokens, reasoningTokens) {
1024
- let billableInputTokens;
1025
- if (inputTokens !== void 0) billableInputTokens = cachedInputTokens !== void 0 ? Math.max(0, inputTokens - cachedInputTokens) : inputTokens;
1026
- let billableOutputTokens;
1027
- if (outputTokens !== void 0) billableOutputTokens = reasoningTokens !== void 0 ? Math.max(0, outputTokens - reasoningTokens) : outputTokens;
1028
- return record({
1029
- billableInputTokens,
1030
- billableOutputTokens
1031
- });
1032
- }
1033
1233
  /** OpenAI Chat Completions `usage` */
1034
1234
  function usageFromChatCompletions(raw) {
1035
1235
  const inputTokens = num(raw.prompt_tokens);
@@ -1041,8 +1241,7 @@ function usageFromChatCompletions(raw) {
1041
1241
  outputTokens,
1042
1242
  totalTokens: num(raw.total_tokens) ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0),
1043
1243
  cachedInputTokens,
1044
- reasoningTokens,
1045
- ...billableFromOpenAIStyle(inputTokens, outputTokens, cachedInputTokens, reasoningTokens)
1244
+ reasoningTokens
1046
1245
  });
1047
1246
  }
1048
1247
  /** OpenAI Responses API `usage` */
@@ -1056,8 +1255,7 @@ function usageFromOpenAIResponses(raw) {
1056
1255
  outputTokens,
1057
1256
  totalTokens: num(raw.total_tokens) ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0),
1058
1257
  cachedInputTokens,
1059
- reasoningTokens,
1060
- ...billableFromOpenAIStyle(inputTokens, outputTokens, cachedInputTokens, reasoningTokens)
1258
+ reasoningTokens
1061
1259
  });
1062
1260
  }
1063
1261
  /** Anthropic Messages `usage`(message_start / message_delta) */
@@ -1072,29 +1270,22 @@ function usageFromAnthropicMessages(raw) {
1072
1270
  cachedInputTokens
1073
1271
  ].filter((n) => n !== void 0);
1074
1272
  const inputTokens = inputParts.length > 0 ? inputParts.reduce((sum, n) => sum + n, 0) : void 0;
1075
- const totalTokens = inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0;
1076
- let billableInputTokens;
1077
- if (uncachedInputTokens !== void 0 || cacheWriteInputTokens !== void 0) billableInputTokens = (uncachedInputTokens ?? 0) + (cacheWriteInputTokens ?? 0);
1078
1273
  return record({
1079
1274
  inputTokens,
1080
1275
  outputTokens,
1081
- totalTokens,
1276
+ totalTokens: inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0,
1082
1277
  cachedInputTokens,
1083
- cacheWriteInputTokens,
1084
- billableInputTokens,
1085
- billableOutputTokens: outputTokens
1278
+ cacheWriteInputTokens
1086
1279
  });
1087
1280
  }
1088
- /** Ollama 流式 chunk(无 cache / reasoning 细分时仅填基础与 billable 镜像) */
1281
+ /** Ollama 流式 chunk */
1089
1282
  function usageFromOllama(raw) {
1090
1283
  const inputTokens = num(raw.prompt_eval_count);
1091
1284
  const outputTokens = num(raw.eval_count);
1092
1285
  return record({
1093
1286
  inputTokens,
1094
1287
  outputTokens,
1095
- totalTokens: inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0,
1096
- billableInputTokens: inputTokens,
1097
- billableOutputTokens: outputTokens
1288
+ totalTokens: inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0
1098
1289
  });
1099
1290
  }
1100
1291
  //#endregion
@@ -1109,43 +1300,55 @@ function usageFromOllama(raw) {
1109
1300
  * - 未完成的行保留在 rest 中,等待下次 chunk 补全
1110
1301
  * - 支持跨 chunk 的 event 分片
1111
1302
  */
1112
- function parseSSEEvents(chunk) {
1303
+ function parseSSEEvents(chunk, options = {}) {
1113
1304
  const events = [];
1114
1305
  let eventType = "";
1115
1306
  let dataLines = [];
1116
1307
  let consumedUntil = 0;
1117
1308
  let cursor = 0;
1118
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
+ };
1119
1337
  while (cursor < chunk.length) {
1120
1338
  const lineEnd = chunk.indexOf("\n", cursor);
1121
1339
  if (lineEnd === -1) break;
1122
1340
  let line = chunk.slice(cursor, lineEnd);
1123
1341
  cursor = lineEnd + 1;
1124
1342
  if (line.endsWith("\r")) line = line.slice(0, -1);
1125
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1126
- else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1127
- else if (line === "" && eventType && dataLines.length > 0) {
1128
- const dataStr = dataLines.join("\n");
1129
- if (dataStr === "[DONE]") {
1130
- eventType = "";
1131
- dataLines = [];
1132
- consumedUntil = cursor;
1133
- continue;
1134
- }
1135
- try {
1136
- const data = JSON.parse(dataStr);
1137
- events.push({
1138
- type: eventType,
1139
- data
1140
- });
1141
- } catch {
1142
- malformedEvents++;
1143
- }
1144
- eventType = "";
1145
- dataLines = [];
1146
- consumedUntil = cursor;
1147
- } else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = cursor;
1343
+ consumeLine(line, cursor);
1148
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;
1350
+ }
1351
+ if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
1149
1352
  return {
1150
1353
  events,
1151
1354
  rest: chunk.slice(consumedUntil),
@@ -1153,70 +1356,287 @@ function parseSSEEvents(chunk) {
1153
1356
  };
1154
1357
  }
1155
1358
  //#endregion
1156
- //#region src/adapters/responses.ts
1359
+ //#region src/helpers/synthetic-stream.ts
1157
1360
  /**
1158
- * Responses Adapter
1361
+ * 模拟流式 (Synthetic Streaming)
1159
1362
  *
1160
- * 接入 OpenAI Responses API (responses 端点)。
1161
- * 职责分层:
1162
- * 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
1163
- * 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
1363
+ * 将一组已解析的 canonical OutputItem 包装为规范事件流。
1364
+ * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
1365
+ * 即可产出一致的事件序列,无需自己逐事件组装。
1164
1366
  *
1165
- * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1367
+ * 约束:
1368
+ * - 每个 item 只发一块完整 delta(不模拟逐 token)
1369
+ * - 保持 item 边界
1370
+ * - 保持后端原始顺序
1371
+ * - 不发明 reasoning
1372
+ * - 不改写工具参数
1166
1373
  */
1167
- function ensureResponsesTextBlocks(blocks, field) {
1168
- for (let i = 0; i < blocks.length; i++) {
1169
- const block = blocks[i];
1170
- if (!block) continue;
1171
- 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");
1172
- }
1173
- return blocks;
1174
- }
1175
- function ensureResponsesReasoningBlocks(blocks, field) {
1176
- return blocks.map((block, index) => {
1177
- 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");
1178
- return block;
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
+ * ```
1389
+ */
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
+ }
1179
1424
  });
1180
1425
  }
1181
- function instructionsToResponsesText(instructions) {
1182
- 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
+ }
1183
1439
  }
1184
- function assertResponsesToolResultOutcome(outcome) {
1185
- 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);
1186
1445
  }
1187
- function parseSSE(chunk) {
1188
- const result = parseSSEEvents(chunk);
1189
- return {
1190
- events: result.events,
1191
- rest: result.rest,
1192
- malformedEvents: result.malformedEvents
1193
- };
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);
1194
1451
  }
1195
- function isReplayCanonicalInput(item) {
1196
- 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);
1197
1456
  }
1198
- function rollbackTrailingReplayCanonicalItems(input) {
1199
- while (input.length > 0) {
1200
- const last = input[input.length - 1];
1201
- if (!last || !isReplayCanonicalInput(last)) break;
1202
- 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
+ };
1203
1493
  }
1204
- }
1205
- function canonicalToResponsesBlock(b) {
1206
- if (b.type === "text") return {
1207
- type: "text",
1208
- text: b.text
1209
- };
1210
- if (b.type === "json") return {
1211
- type: "text",
1212
- text: JSON.stringify(b.json)
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)
1213
1511
  };
1214
- throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
1215
1512
  }
1216
- var ResponsesAdapter = class extends AdapterBase {
1217
- kind = "responses";
1218
- nativeStreaming = true;
1219
- apiKey;
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)
1530
+ };
1531
+ }
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;
1220
1640
  baseUrl;
1221
1641
  fetchFn;
1222
1642
  constructor(options) {
@@ -1230,7 +1650,7 @@ var ResponsesAdapter = class extends AdapterBase {
1230
1650
  for (const item of request.input) switch (item.type) {
1231
1651
  case "message":
1232
1652
  if (item.role === "assistant") {
1233
- 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);
1234
1654
  input.push({
1235
1655
  type: "message",
1236
1656
  role: item.role,
@@ -1239,11 +1659,11 @@ var ResponsesAdapter = class extends AdapterBase {
1239
1659
  } else input.push({
1240
1660
  type: "message",
1241
1661
  role: item.role,
1242
- content: contentBlocksToText(ensureResponsesTextBlocks(item.content, `input message (${item.role}) content`))
1662
+ content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
1243
1663
  });
1244
1664
  break;
1245
1665
  case "reasoning": {
1246
- const blocks = ensureResponsesReasoningBlocks(item.content, "reasoning content").map((b) => ({
1666
+ const blocks = mapper$3.ensureReasoningBlocks(item.content, "reasoning content").map((b) => ({
1247
1667
  type: "reasoning",
1248
1668
  text: b.text
1249
1669
  }));
@@ -1262,8 +1682,8 @@ var ResponsesAdapter = class extends AdapterBase {
1262
1682
  });
1263
1683
  break;
1264
1684
  case "tool_result": {
1265
- assertResponsesToolResultOutcome(item.outcome);
1266
- 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");
1267
1687
  input.push({
1268
1688
  type: "function_call_output",
1269
1689
  call_id: item.callId,
@@ -1271,25 +1691,26 @@ var ResponsesAdapter = class extends AdapterBase {
1271
1691
  });
1272
1692
  break;
1273
1693
  }
1274
- case "opaque":
1275
- if (item.source === "responses" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null && "id" in item.payload) {
1276
- const { id } = item.payload;
1277
- if (typeof id === "string") {
1278
- rollbackTrailingReplayCanonicalItems(input);
1279
- input.push({
1280
- type: "item_reference",
1281
- id
1282
- });
1283
- }
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
+ });
1284
1704
  }
1285
1705
  break;
1706
+ }
1286
1707
  }
1287
1708
  const body = {
1288
1709
  model: request.model,
1289
1710
  input,
1290
1711
  stream: true
1291
1712
  };
1292
- if (request.instructions) body.instructions = instructionsToResponsesText(request.instructions);
1713
+ if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
1293
1714
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1294
1715
  type: "function",
1295
1716
  name: t.name,
@@ -1311,31 +1732,59 @@ var ResponsesAdapter = class extends AdapterBase {
1311
1732
  }
1312
1733
  async *runStream(providerRequest, factory, request) {
1313
1734
  const auxiliary = this.createAuxiliaryState(request);
1314
- const response = await this.fetchFn(`${this.baseUrl}/responses`, {
1315
- method: "POST",
1316
- headers: {
1317
- "Content-Type": "application/json",
1318
- Authorization: `Bearer ${this.apiKey}`
1319
- },
1320
- body: JSON.stringify(providerRequest)
1321
- });
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
+ }
1322
1748
  if (!response.ok) {
1323
- const errorText = await response.text().catch(() => "unknown error");
1324
- throw new Error(`Responses API error ${response.status}: ${errorText}`);
1749
+ const errorBody = await response.text().catch(() => "");
1750
+ throw providerHttpError(response.status, errorBody);
1325
1751
  }
1326
1752
  const reader = response.body?.getReader();
1327
- 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
+ });
1328
1776
  const output = [];
1329
- const decoder = new TextDecoder();
1330
- let buffer = "";
1777
+ let streamDone = false;
1331
1778
  let completedResponse;
1779
+ let completedEmitted = false;
1780
+ let unknownEventsWarned = false;
1781
+ const messageItemsWithDelta = /* @__PURE__ */ new Set();
1332
1782
  try {
1333
1783
  while (true) {
1334
- const { done, value } = await reader.read();
1335
- if (done) break;
1336
- buffer += decoder.decode(value, { stream: true });
1337
- const { events, rest, malformedEvents } = parseSSE(buffer);
1338
- 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);
1339
1788
  const malformedWarning = emitMalformedStreamWarning(factory, {
1340
1789
  count: malformedEvents,
1341
1790
  providerLabel: "Responses",
@@ -1344,7 +1793,8 @@ var ResponsesAdapter = class extends AdapterBase {
1344
1793
  if (malformedWarning) yield malformedWarning;
1345
1794
  for (const sseEvent of events) {
1346
1795
  if (sseEvent.type === "error") {
1347
- 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);
1348
1798
  continue;
1349
1799
  }
1350
1800
  if (sseEvent.type === "response.output_item.added") {
@@ -1363,40 +1813,69 @@ var ResponsesAdapter = class extends AdapterBase {
1363
1813
  continue;
1364
1814
  }
1365
1815
  if (sseEvent.type === "response.output_text.delta") {
1366
- 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);
1367
1819
  continue;
1368
1820
  }
1369
1821
  if (sseEvent.type === "response.output_text.done") {
1370
- yield factory.messageCompleted(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
1371
- 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 }));
1372
1826
  continue;
1373
1827
  }
1374
1828
  if (sseEvent.type === "response.reasoning.delta") {
1375
- 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));
1376
1831
  continue;
1377
1832
  }
1378
1833
  if (sseEvent.type === "response.reasoning.done") {
1379
- yield factory.reasoningCompleted(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
1380
- 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));
1381
1837
  continue;
1382
1838
  }
1383
1839
  if (sseEvent.type === "response.tool_call.delta") {
1384
- 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 });
1385
1842
  continue;
1386
1843
  }
1387
1844
  if (sseEvent.type === "response.tool_call.done") {
1388
- const tcItem = toolCallItem(sseEvent.data.item_id, sseEvent.data.name ?? "unknown", sseEvent.data.arguments ?? "");
1389
- 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);
1390
1848
  output.push(tcItem);
1391
1849
  continue;
1392
1850
  }
1393
- 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;
1394
1869
  }
1395
1870
  }
1396
1871
  } finally {
1397
- reader.releaseLock();
1872
+ try {
1873
+ if (!streamDone) await reader.cancel().catch(() => void 0);
1874
+ } finally {
1875
+ reader.releaseLock();
1876
+ }
1398
1877
  }
1399
- 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");
1400
1879
  let rawResponseId;
1401
1880
  if (completedResponse) {
1402
1881
  rawResponseId = completedResponse.id;
@@ -1407,23 +1886,44 @@ var ResponsesAdapter = class extends AdapterBase {
1407
1886
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
1408
1887
  const auxiliaryResult = await auxiliary.finalize(factory);
1409
1888
  for (const event of auxiliaryResult.events) yield event;
1410
- yield factory.responseCompleted(this.buildResponse(request, {
1411
- output,
1412
- replay,
1413
- stopReason,
1414
- usage: auxiliaryResult.usage,
1415
- billing: auxiliaryResult.billing,
1416
- auxiliary: auxiliaryResult.auxiliary,
1417
- warnings: auxiliaryResult.warnings,
1418
- metadataSources: auxiliaryResult.metadataSources,
1419
- rawResponseId
1420
- }, 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
+ }
1421
1912
  }
1422
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
+ }
1423
1921
  const output = response.output;
1424
- if (!output || output.length === 0) return "unknown";
1922
+ if (!output || output.length === 0) return response.status === "completed" ? "end_turn" : "unknown";
1425
1923
  if (output.some((item) => item.type === "function_call")) return "tool_call";
1426
- 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";
1427
1927
  return "end_turn";
1428
1928
  }
1429
1929
  };
@@ -1440,36 +1940,41 @@ var ResponsesAdapter = class extends AdapterBase {
1440
1940
  * - 高保真 replay(含 opaque continuation)
1441
1941
  * - 能力降级 warning
1442
1942
  */
1443
- function ensureMessagesTextBlocks(blocks, field) {
1444
- for (let i = 0; i < blocks.length; i++) {
1445
- const block = blocks[i];
1446
- if (!block) continue;
1447
- 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");
1448
- }
1449
- return blocks;
1450
- }
1451
- function ensureMessagesReasoningBlocks(blocks, field) {
1452
- return blocks.map((block, index) => {
1453
- 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");
1454
- return block;
1455
- });
1456
- }
1457
- function instructionsToMessagesText(instructions) {
1458
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
1459
- }
1460
- function assertMessagesToolResultOutcome(outcome) {
1461
- if (outcome === "rejected") throw new AIRequestError("messages does not preserve tool_result outcome \"rejected\"; only \"success\" and \"error\" are supported", "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1462
- }
1463
- function parseMessagesSSE(chunk) {
1464
- const result = parseSSEEvents(chunk);
1465
- return {
1466
- events: result.events,
1467
- rest: result.rest,
1468
- malformedEvents: result.malformedEvents
1469
- };
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
+ }
1470
1974
  }
1471
- function rollbackTrailingAssistantMessages$2(messages) {
1472
- 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");
1473
1978
  }
1474
1979
  /** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
1475
1980
  function synthesizeItemId(kind, blockIndex, responseId) {
@@ -1519,34 +2024,29 @@ function buildStreamMetadata(options) {
1519
2024
  }
1520
2025
  var MessagesAdapter = class extends AdapterBase {
1521
2026
  kind = "messages";
1522
- nativeStreaming = true;
2027
+ capabilities = profile$2.capabilities;
1523
2028
  apiKey;
1524
2029
  apiVersion;
1525
2030
  baseUrl;
1526
2031
  fetchFn;
1527
- warningAccumulator;
1528
2032
  constructor(options) {
1529
2033
  super();
1530
2034
  this.apiKey = options.apiKey;
1531
2035
  this.apiVersion = options.apiVersion ?? "2023-06-01";
1532
2036
  this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
1533
2037
  this.fetchFn = options.fetch ?? globalThis.fetch;
1534
- this.warningAccumulator = [];
1535
- }
1536
- warn(message, _code) {
1537
- this.warningAccumulator.push(message);
1538
2038
  }
1539
2039
  buildRequest(request) {
1540
2040
  const messages = [];
1541
2041
  let systemPrompt;
1542
2042
  let pendingToolResultMessage;
1543
- if (request.instructions) systemPrompt = instructionsToMessagesText(request.instructions);
2043
+ if (request.instructions) systemPrompt = mapper$2.mapInstructions(request.instructions);
1544
2044
  for (const item of request.input) {
1545
2045
  if (item.type !== "tool_result") pendingToolResultMessage = void 0;
1546
2046
  switch (item.type) {
1547
2047
  case "message": {
1548
2048
  const role = item.role === "user" ? "user" : "assistant";
1549
- const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
2049
+ const supportedContent = mapper$2.ensureTextBlocks(item.content, `input message (${item.role}) content`);
1550
2050
  if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
1551
2051
  role,
1552
2052
  content: supportedContent[0].text
@@ -1573,8 +2073,8 @@ var MessagesAdapter = class extends AdapterBase {
1573
2073
  break;
1574
2074
  }
1575
2075
  case "tool_result": {
1576
- assertMessagesToolResultOutcome(item.outcome);
1577
- 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");
1578
2078
  const block = {
1579
2079
  type: "tool_result",
1580
2080
  tool_use_id: item.callId,
@@ -1594,7 +2094,7 @@ var MessagesAdapter = class extends AdapterBase {
1594
2094
  case "reasoning": {
1595
2095
  const block = {
1596
2096
  type: "thinking",
1597
- thinking: contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"))
2097
+ thinking: contentBlocksToText(mapper$2.ensureReasoningBlocks(item.content, "reasoning content"))
1598
2098
  };
1599
2099
  const lastMsg = messages[messages.length - 1];
1600
2100
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
@@ -1604,20 +2104,20 @@ var MessagesAdapter = class extends AdapterBase {
1604
2104
  });
1605
2105
  break;
1606
2106
  }
1607
- case "opaque":
1608
- if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
1609
- const payload = item.payload;
1610
- if (payload.role === "assistant" && Array.isArray(payload.content)) {
1611
- 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"))) {
1612
- rollbackTrailingAssistantMessages$2(messages);
1613
- messages.push({
1614
- role: "assistant",
1615
- content: payload.content
1616
- });
1617
- }
1618
- }
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
+ });
1619
2118
  }
1620
2119
  break;
2120
+ }
1621
2121
  }
1622
2122
  }
1623
2123
  const body = {
@@ -1644,27 +2144,53 @@ var MessagesAdapter = class extends AdapterBase {
1644
2144
  return body;
1645
2145
  }
1646
2146
  async *runStream(providerRequest, factory, request) {
1647
- this.warningAccumulator = [];
1648
2147
  const auxiliary = this.createAuxiliaryState(request);
2148
+ let completedEmitted = false;
1649
2149
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
1650
- const response = await this.fetchFn(`${this.baseUrl}/messages`, {
1651
- method: "POST",
1652
- headers: {
1653
- "Content-Type": "application/json",
1654
- "x-api-key": this.apiKey,
1655
- "anthropic-version": this.apiVersion
1656
- },
1657
- body: JSON.stringify(providerRequest)
1658
- });
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
+ }
1659
2164
  if (!response.ok) {
1660
- const errorText = await response.text().catch(() => "unknown error");
1661
- throw new Error(`Messages API error ${response.status}: ${errorText}`);
2165
+ const errorBody = await response.text().catch(() => "");
2166
+ throw providerHttpError(response.status, errorBody);
1662
2167
  }
1663
2168
  const reader = response.body?.getReader();
1664
- 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
+ });
1665
2192
  const output = [];
1666
- const decoder = new TextDecoder();
1667
- let buffer = "";
2193
+ let streamDone = false;
1668
2194
  let messageResponse;
1669
2195
  let currentContentBlockIndex = -1;
1670
2196
  let currentItemType = null;
@@ -1686,11 +2212,10 @@ var MessagesAdapter = class extends AdapterBase {
1686
2212
  }
1687
2213
  try {
1688
2214
  while (true) {
1689
- const { done, value } = await reader.read();
1690
- if (done) break;
1691
- buffer += decoder.decode(value, { stream: true });
1692
- const { events, rest, malformedEvents } = parseMessagesSSE(buffer);
1693
- 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);
1694
2219
  const malformedWarning = emitMalformedStreamWarning(factory, {
1695
2220
  count: malformedEvents,
1696
2221
  providerLabel: "Messages",
@@ -1702,7 +2227,6 @@ var MessagesAdapter = class extends AdapterBase {
1702
2227
  case "error": {
1703
2228
  const err = sseEvent.data.error;
1704
2229
  yield factory.responseWarning(err.message, err.type);
1705
- this.warn(err.message, err.type);
1706
2230
  continue;
1707
2231
  }
1708
2232
  case "message_start":
@@ -1737,7 +2261,7 @@ var MessagesAdapter = class extends AdapterBase {
1737
2261
  yield factory.reasoningStarted(currentItemId, "redacted");
1738
2262
  yield factory.reasoningDelta(currentItemId, textBlock(data));
1739
2263
  const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
1740
- yield factory.reasoningCompleted(redactedItem);
2264
+ yield factory.reasoningCompleted(currentItemId);
1741
2265
  output.push(redactedItem);
1742
2266
  rawReplayContent.push({
1743
2267
  type: "redacted_thinking",
@@ -1766,7 +2290,7 @@ var MessagesAdapter = class extends AdapterBase {
1766
2290
  if (currentItemType === "message" && currentItemId) {
1767
2291
  const txt = delta.text;
1768
2292
  textBuffer += txt;
1769
- yield factory.messageDelta(currentItemId, txt);
2293
+ yield factory.messageDelta(currentItemId, textBlock(txt));
1770
2294
  }
1771
2295
  break;
1772
2296
  case "thinking_delta":
@@ -1788,14 +2312,14 @@ var MessagesAdapter = class extends AdapterBase {
1788
2312
  }
1789
2313
  case "content_block_stop":
1790
2314
  if (currentItemType === "message" && currentItemId) {
1791
- yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
2315
+ yield factory.messageCompleted(currentItemId);
1792
2316
  output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
1793
2317
  rawReplayContent.push({
1794
2318
  type: "text",
1795
2319
  text: textBuffer
1796
2320
  });
1797
2321
  } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
1798
- yield factory.reasoningCompleted(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
2322
+ yield factory.reasoningCompleted(currentItemId);
1799
2323
  output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
1800
2324
  rawReplayContent.push({
1801
2325
  type: "thinking",
@@ -1803,7 +2327,7 @@ var MessagesAdapter = class extends AdapterBase {
1803
2327
  });
1804
2328
  } else if (currentItemType === "tool_call" && currentItemId) {
1805
2329
  const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
1806
- yield factory.toolCallCompleted(tcItem);
2330
+ yield factory.toolCallCompleted(currentItemId);
1807
2331
  output.push(tcItem);
1808
2332
  rawReplayContent.push({
1809
2333
  type: "tool_use",
@@ -1824,11 +2348,19 @@ var MessagesAdapter = class extends AdapterBase {
1824
2348
  }
1825
2349
  case "message_stop": break;
1826
2350
  }
2351
+ if (done) {
2352
+ streamDone = true;
2353
+ break;
2354
+ }
1827
2355
  }
1828
2356
  } finally {
1829
- reader.releaseLock();
2357
+ try {
2358
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2359
+ } finally {
2360
+ reader.releaseLock();
2361
+ }
1830
2362
  }
1831
- 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");
1832
2364
  const replay = [...replayFromOutput(output)];
1833
2365
  if (messageResponse) {
1834
2366
  const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
@@ -1849,17 +2381,29 @@ var MessagesAdapter = class extends AdapterBase {
1849
2381
  if (!hasStreamedReasoning) {}
1850
2382
  const auxiliaryResult = await auxiliary.finalize(factory);
1851
2383
  for (const event of auxiliaryResult.events) yield event;
1852
- yield factory.responseCompleted(this.buildResponse(request, {
1853
- output,
1854
- replay,
1855
- stopReason: stopReason ? mapStopReason(stopReason) : void 0,
1856
- usage: auxiliaryResult.usage,
1857
- billing: auxiliaryResult.billing,
1858
- auxiliary: auxiliaryResult.auxiliary,
1859
- warnings: auxiliaryResult.warnings,
1860
- metadataSources: auxiliaryResult.metadataSources,
1861
- rawResponseId
1862
- }, 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
+ }
1863
2407
  }
1864
2408
  };
1865
2409
  //#endregion
@@ -1874,51 +2418,21 @@ var MessagesAdapter = class extends AdapterBase {
1874
2418
  * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
1875
2419
  */
1876
2420
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
1877
- function assertChatToolResultOutcome(outcome) {
1878
- if (outcome !== "success") throw new AIRequestError(`chat-completions does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1879
- }
1880
- /**
1881
- * Chat Completions 的简化 SSE 解析器。
1882
- *
1883
- * 约束:
1884
- * - 每条 `data:` 行必须已经是一个完整 JSON 对象
1885
- * - 允许传输层把单行拆成多个 chunk,但不接受 provider 把一个 JSON event 改写成多条 `data:` 行
1886
- */
1887
- function parseChatSSE(buffer) {
1888
- const chunks = [];
1889
- let rest = buffer;
1890
- let malformedEvents = 0;
1891
- while (true) {
1892
- const lineEnd = rest.indexOf("\n");
1893
- if (lineEnd === -1) break;
1894
- const line = rest.slice(0, lineEnd).trim();
1895
- rest = rest.slice(lineEnd + 1);
1896
- if (!line.startsWith("data: ")) continue;
1897
- const data = line.slice(6).trim();
1898
- if (data === "[DONE]") continue;
1899
- try {
1900
- chunks.push(JSON.parse(data));
1901
- } catch {
1902
- malformedEvents++;
1903
- }
1904
- }
1905
- return {
1906
- chunks,
1907
- rest,
1908
- malformedEvents
1909
- };
1910
- }
1911
- function ensureTextCompatibleBlocks(blocks, field) {
1912
- for (let i = 0; i < blocks.length; i++) {
1913
- const block = blocks[i];
1914
- if (!block) continue;
1915
- 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");
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"]
1916
2433
  }
1917
- return blocks;
1918
- }
1919
- function contentBlocksToChatText(blocks, field) {
1920
- return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
1921
- }
2434
+ };
2435
+ const mapper$1 = new NormalizedRequestMapper(profile$1);
1922
2436
  function extractReasoningText(value) {
1923
2437
  if (typeof value === "string") return value;
1924
2438
  if (Array.isArray(value)) return value.map(extractReasoningText).join("");
@@ -1949,8 +2463,31 @@ function extractReasoningDeltas(delta) {
1949
2463
  }
1950
2464
  return deltas;
1951
2465
  }
1952
- function rollbackTrailingAssistantMessages$1(messages) {
1953
- 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");
1954
2491
  }
1955
2492
  function buildAssistantReplayMessage(params) {
1956
2493
  const { content, reasoningByField, toolCalls } = params;
@@ -1972,7 +2509,7 @@ function buildAssistantReplayMessage(params) {
1972
2509
  }
1973
2510
  var ChatCompletionsAdapter = class extends AdapterBase {
1974
2511
  kind = "chat-completions";
1975
- nativeStreaming = true;
2512
+ capabilities = profile$1.capabilities;
1976
2513
  apiKey;
1977
2514
  baseUrl;
1978
2515
  fetchFn;
@@ -1984,17 +2521,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
1984
2521
  }
1985
2522
  buildRequest(request) {
1986
2523
  const messages = [];
1987
- if (request.instructions) {
1988
- const content = typeof request.instructions === "string" ? request.instructions : contentBlocksToChatText(request.instructions, "instructions");
1989
- messages.push({
1990
- role: "system",
1991
- content
1992
- });
1993
- }
2524
+ if (request.instructions) messages.push({
2525
+ role: "system",
2526
+ content: mapper$1.mapInstructions(request.instructions)
2527
+ });
1994
2528
  for (const item of request.input) switch (item.type) {
1995
2529
  case "message": {
1996
2530
  const role = item.role;
1997
- const text = contentBlocksToChatText(item.content, `input message (${item.role}) content`);
2531
+ const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
1998
2532
  messages.push({
1999
2533
  role,
2000
2534
  content: text || null
@@ -2020,38 +2554,44 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2020
2554
  break;
2021
2555
  }
2022
2556
  case "tool_result":
2023
- assertChatToolResultOutcome(item.outcome);
2557
+ mapper$1.assertToolResultOutcome(item.outcome);
2024
2558
  messages.push({
2025
2559
  role: "tool",
2026
2560
  tool_call_id: item.callId,
2027
2561
  name: item.toolName,
2028
- content: contentBlocksToChatText(item.content, `tool_result ${item.callId} content`)
2562
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2029
2563
  });
2030
2564
  break;
2031
2565
  case "reasoning":
2032
2566
  messages.push({
2033
2567
  role: "assistant",
2034
- content: contentBlocksToChatText(item.content, "reasoning content")
2568
+ content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
2035
2569
  });
2036
2570
  break;
2037
- case "opaque":
2038
- if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2039
- const payload = item.payload;
2040
- if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
2041
- role: "assistant",
2042
- content: payload.content
2043
- });
2044
- else if (payload.replaceCanonical === true && Array.isArray(payload.messages)) {
2045
- rollbackTrailingAssistantMessages$1(messages);
2046
- for (const m of payload.messages) messages.push(m);
2047
- } 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);
2048
2586
  }
2049
2587
  break;
2588
+ }
2050
2589
  }
2051
2590
  const body = {
2052
2591
  model: request.model,
2053
2592
  messages,
2054
- stream: true
2593
+ stream: true,
2594
+ n: 1
2055
2595
  };
2056
2596
  if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2057
2597
  type: "function",
@@ -2076,23 +2616,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2076
2616
  }
2077
2617
  async *runStream(providerRequest, factory, request) {
2078
2618
  const auxiliary = this.createAuxiliaryState(request);
2079
- const response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2080
- method: "POST",
2081
- headers: {
2082
- "Content-Type": "application/json",
2083
- Authorization: `Bearer ${this.apiKey}`
2084
- },
2085
- body: JSON.stringify(providerRequest)
2086
- });
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
+ }
2087
2632
  if (!response.ok) {
2088
- const errorText = await response.text().catch(() => "unknown error");
2089
- throw new Error(`Chat Completions API error ${response.status}: ${errorText}`);
2633
+ const errorBody = await response.text().catch(() => "");
2634
+ throw providerHttpError(response.status, errorBody);
2090
2635
  }
2091
2636
  const reader = response.body?.getReader();
2092
- 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
+ });
2093
2652
  const output = [];
2094
- const decoder = new TextDecoder();
2095
- let buffer = "";
2653
+ let streamDone = false;
2096
2654
  let responseId;
2097
2655
  let accumulatedContent = "";
2098
2656
  let accumulatedReasoning = "";
@@ -2100,6 +2658,9 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2100
2658
  let currentReasoningId = "";
2101
2659
  let hasMessageStarted = false;
2102
2660
  let hasReasoningStarted = false;
2661
+ let completedEmitted = false;
2662
+ let warnedNonZeroChoice = false;
2663
+ const buildResponse = this.buildResponse.bind(this);
2103
2664
  const pendingToolCalls = /* @__PURE__ */ new Map();
2104
2665
  const reasoningByField = /* @__PURE__ */ new Map();
2105
2666
  const finalizePendingTurn = () => {
@@ -2108,17 +2669,17 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2108
2669
  const finalizedReasoningByField = new Map(reasoningByField);
2109
2670
  if (hasReasoningStarted && accumulatedReasoning) {
2110
2671
  const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
2111
- events.push(factory.reasoningCompleted(reasoning));
2672
+ events.push(factory.reasoningCompleted(currentReasoningId));
2112
2673
  output.push(reasoning);
2113
2674
  }
2114
- if (hasMessageStarted && accumulatedContent) {
2675
+ if (hasMessageStarted) {
2115
2676
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2116
- events.push(factory.messageCompleted(message));
2117
- output.push(message);
2677
+ events.push(factory.messageCompleted(currentMessageId));
2678
+ if (accumulatedContent) output.push(message);
2118
2679
  }
2119
2680
  for (const pending of finalizedToolCalls) {
2120
2681
  const toolCall = toolCallItem(pending.id, pending.name, pending.args);
2121
- events.push(factory.toolCallCompleted(toolCall));
2682
+ events.push(factory.toolCallCompleted(pending.id));
2122
2683
  output.push(toolCall);
2123
2684
  }
2124
2685
  const assistantReplayMessage = buildAssistantReplayMessage({
@@ -2139,13 +2700,46 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2139
2700
  assistantReplayMessage
2140
2701
  };
2141
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
+ };
2142
2737
  try {
2143
2738
  while (true) {
2144
- const { done, value } = await reader.read();
2145
- if (done) break;
2146
- buffer += decoder.decode(value, { stream: true });
2147
- const { chunks, rest, malformedEvents } = parseChatSSE(buffer);
2148
- 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);
2149
2743
  const malformedWarning = emitMalformedStreamWarning(factory, {
2150
2744
  count: malformedEvents,
2151
2745
  providerLabel: "Chat Completions",
@@ -2156,14 +2750,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2156
2750
  responseId = chunk.id;
2157
2751
  if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
2158
2752
  for (const choice of chunk.choices) {
2159
- 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
+ }
2160
2764
  const delta = choice.delta;
2161
2765
  const finishReason = choice.finish_reason;
2162
2766
  const reasoningDeltas = extractReasoningDeltas(delta);
2163
- if (delta.role === "assistant" && typeof delta.content === "string" && !hasMessageStarted) {
2767
+ const ensureMessageStarted = () => {
2768
+ if (hasMessageStarted) return;
2164
2769
  currentMessageId = `msg-${chunk.id}`;
2165
2770
  hasMessageStarted = true;
2166
2771
  accumulatedContent = "";
2772
+ };
2773
+ if (delta.role === "assistant" && !hasMessageStarted) {
2774
+ ensureMessageStarted();
2167
2775
  yield factory.messageStarted(currentMessageId);
2168
2776
  }
2169
2777
  if (reasoningDeltas.length > 0) {
@@ -2181,32 +2789,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2181
2789
  }
2182
2790
  if (delta.content) {
2183
2791
  if (!hasMessageStarted) {
2184
- currentMessageId = `msg-${chunk.id}`;
2185
- hasMessageStarted = true;
2792
+ ensureMessageStarted();
2186
2793
  yield factory.messageStarted(currentMessageId);
2187
2794
  }
2188
2795
  accumulatedContent += delta.content;
2189
- yield factory.messageDelta(currentMessageId, delta.content);
2796
+ yield factory.messageDelta(currentMessageId, textBlock(delta.content));
2190
2797
  }
2191
- if (delta.tool_calls) for (const tc of delta.tool_calls) {
2192
- const idx = tc.index;
2193
- if (tc.id) {
2194
- pendingToolCalls.set(idx, {
2195
- id: tc.id,
2196
- name: tc.function?.name ?? "",
2197
- args: ""
2198
- });
2199
- yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2798
+ if (delta.tool_calls) {
2799
+ if (!hasMessageStarted) {
2800
+ ensureMessageStarted();
2801
+ yield factory.messageStarted(currentMessageId);
2200
2802
  }
2201
- if (tc.function?.arguments) {
2202
- const pending = pendingToolCalls.get(idx);
2203
- if (pending) {
2204
- pending.args += tc.function.arguments;
2205
- 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
+ }
2206
2819
  }
2207
2820
  }
2208
2821
  }
2209
2822
  if (delta.function_call) {
2823
+ if (!hasMessageStarted) {
2824
+ ensureMessageStarted();
2825
+ yield factory.messageStarted(currentMessageId);
2826
+ }
2210
2827
  if (delta.function_call.name) {
2211
2828
  const fcId = `fc-${chunk.id}-0`;
2212
2829
  pendingToolCalls.set(0, {
@@ -2227,54 +2844,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2227
2844
  if (finishReason && finishReason !== null) {
2228
2845
  const { events, assistantReplayMessage } = finalizePendingTurn();
2229
2846
  for (const event of events) yield event;
2230
- const stopReason = mapStopReason(finishReason);
2231
- const replay = [...replayFromOutput(output)];
2232
- if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2233
- replaceCanonical: true,
2234
- messages: [assistantReplayMessage]
2235
- }));
2236
- const auxiliaryResult = await auxiliary.finalize(factory);
2237
- for (const event of auxiliaryResult.events) yield event;
2238
- yield factory.responseCompleted(this.buildResponse(request, {
2239
- output,
2240
- replay,
2241
- stopReason,
2242
- usage: auxiliaryResult.usage,
2243
- billing: auxiliaryResult.billing,
2244
- auxiliary: auxiliaryResult.auxiliary,
2245
- warnings: auxiliaryResult.warnings,
2246
- metadataSources: auxiliaryResult.metadataSources,
2247
- rawResponseId: chunk.id
2248
- }, factory));
2847
+ yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
2249
2848
  }
2250
2849
  }
2251
2850
  }
2851
+ if (done) {
2852
+ streamDone = true;
2853
+ break;
2854
+ }
2252
2855
  }
2253
2856
  } finally {
2254
- reader.releaseLock();
2857
+ try {
2858
+ if (!streamDone) await reader.cancel().catch(() => void 0);
2859
+ } finally {
2860
+ reader.releaseLock();
2861
+ }
2255
2862
  }
2256
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2257
- 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)) {
2258
2865
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2259
2866
  const { events, assistantReplayMessage } = finalizePendingTurn();
2260
2867
  for (const event of events) yield event;
2261
- const replay = [...replayFromOutput(output)];
2262
- if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2263
- replaceCanonical: true,
2264
- messages: [assistantReplayMessage]
2265
- }));
2266
- const auxiliaryResult = await auxiliary.finalize(factory);
2267
- for (const event of auxiliaryResult.events) yield event;
2268
- yield factory.responseCompleted(this.buildResponse(request, {
2269
- output,
2270
- replay,
2271
- usage: auxiliaryResult.usage,
2272
- billing: auxiliaryResult.billing,
2273
- auxiliary: auxiliaryResult.auxiliary,
2274
- warnings: auxiliaryResult.warnings,
2275
- metadataSources: auxiliaryResult.metadataSources,
2276
- rawResponseId: responseId
2277
- }, factory));
2868
+ yield* emitCompleted(void 0, assistantReplayMessage, responseId);
2278
2869
  }
2279
2870
  }
2280
2871
  };
@@ -2296,23 +2887,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2296
2887
  * - tool_call 不支持逐 token 流式
2297
2888
  * - replay 保真度低(无 opaque continuation 机制)
2298
2889
  */
2299
- function ensureOllamaTextBlocks(blocks, field) {
2300
- for (let i = 0; i < blocks.length; i++) {
2301
- const block = blocks[i];
2302
- if (!block) continue;
2303
- 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");
2304
- }
2305
- return blocks;
2306
- }
2307
- function ensureOllamaReasoningBlocks(blocks, field) {
2308
- return blocks.map((block, index) => {
2309
- 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");
2310
- return block;
2311
- });
2312
- }
2313
- function instructionsToOllamaText(instructions) {
2314
- return typeof instructions === "string" ? instructions : contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
2315
- }
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);
2316
2905
  function parseOllamaToolArguments(item) {
2317
2906
  if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2318
2907
  try {
@@ -2321,46 +2910,24 @@ function parseOllamaToolArguments(item) {
2321
2910
  } catch {}
2322
2911
  throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2323
2912
  }
2324
- function assertOllamaToolResultOutcome(outcome) {
2325
- if (outcome !== "success") throw new AIRequestError(`ollama does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
2326
- }
2327
- function parseOllamaNDJSON(buffer) {
2328
- const chunks = [];
2329
- let rest = buffer;
2330
- let malformedLines = 0;
2331
- while (true) {
2332
- const lineEnd = rest.indexOf("\n");
2333
- if (lineEnd === -1) break;
2334
- const line = rest.slice(0, lineEnd).trim();
2335
- rest = rest.slice(lineEnd + 1);
2336
- if (!line) continue;
2337
- try {
2338
- const parsed = JSON.parse(line);
2339
- if (parsed && typeof parsed === "object" && "message" in parsed) chunks.push(parsed);
2340
- else malformedLines++;
2341
- } catch {
2342
- malformedLines++;
2343
- }
2344
- }
2345
- return {
2346
- chunks,
2347
- rest,
2348
- malformedLines
2349
- };
2350
- }
2351
- function rollbackTrailingAssistantMessages(messages) {
2352
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
2353
- }
2354
- function isOllamaToolCalls(value) {
2913
+ function isOllamaReplayToolCalls(value) {
2355
2914
  return Array.isArray(value) && value.every((entry) => {
2356
2915
  if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
2357
2916
  const fn = entry.function;
2917
+ const id = entry.id;
2918
+ if (id !== void 0 && typeof id !== "string") return false;
2358
2919
  return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
2359
2920
  });
2360
2921
  }
2922
+ function toWireOllamaToolCalls(toolCalls) {
2923
+ return toolCalls.map((tc) => ({ function: {
2924
+ name: tc.function.name,
2925
+ arguments: tc.function.arguments
2926
+ } }));
2927
+ }
2361
2928
  var OllamaAdapter = class extends AdapterBase {
2362
2929
  kind = "ollama";
2363
- nativeStreaming = true;
2930
+ capabilities = profile.capabilities;
2364
2931
  baseUrl;
2365
2932
  apiKey;
2366
2933
  fetchFn;
@@ -2373,16 +2940,18 @@ var OllamaAdapter = class extends AdapterBase {
2373
2940
  buildRequest(request) {
2374
2941
  if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2375
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();
2376
2945
  if (request.instructions) messages.push({
2377
2946
  role: "system",
2378
- content: instructionsToOllamaText(request.instructions)
2947
+ content: mapper.mapInstructions(request.instructions)
2379
2948
  });
2380
2949
  for (const item of request.input) switch (item.type) {
2381
2950
  case "message": {
2382
2951
  const role = item.role;
2383
2952
  messages.push({
2384
2953
  role,
2385
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `input message (${item.role}) content`))
2954
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
2386
2955
  });
2387
2956
  break;
2388
2957
  }
@@ -2392,6 +2961,9 @@ var OllamaAdapter = class extends AdapterBase {
2392
2961
  name: item.name,
2393
2962
  arguments: parseOllamaToolArguments(item)
2394
2963
  } };
2964
+ const queue = callIdsByName.get(item.name) ?? [];
2965
+ queue.push(item.id);
2966
+ callIdsByName.set(item.name, queue);
2395
2967
  if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
2396
2968
  else messages.push({
2397
2969
  role: "assistant",
@@ -2400,32 +2972,48 @@ var OllamaAdapter = class extends AdapterBase {
2400
2972
  });
2401
2973
  break;
2402
2974
  }
2403
- case "tool_result":
2404
- 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();
2405
2979
  messages.push({
2406
2980
  role: "tool",
2407
- content: contentBlocksToText(ensureOllamaTextBlocks(item.content, `tool_result ${item.callId} content`))
2981
+ content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2408
2982
  });
2409
2983
  break;
2984
+ }
2410
2985
  case "reasoning":
2411
2986
  messages.push({
2412
2987
  role: "assistant",
2413
- content: contentBlocksToText(ensureOllamaReasoningBlocks(item.content, "reasoning content"))
2988
+ content: contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"))
2414
2989
  });
2415
2990
  break;
2416
- case "opaque":
2417
- if (item.source === "ollama" && item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
2418
- const payload = item.payload;
2419
- if (payload.role === "assistant" && typeof payload.content === "string") {
2420
- rollbackTrailingAssistantMessages(messages);
2421
- messages.push({
2422
- role: "assistant",
2423
- content: payload.content,
2424
- tool_calls: isOllamaToolCalls(payload.tool_calls) ? payload.tool_calls : void 0
2425
- });
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;
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
+ }
2426
3008
  }
3009
+ messages.push({
3010
+ role: "assistant",
3011
+ content: payload.content,
3012
+ tool_calls: replayToolCalls ? toWireOllamaToolCalls(replayToolCalls) : void 0
3013
+ });
2427
3014
  }
2428
3015
  break;
3016
+ }
2429
3017
  }
2430
3018
  const body = {
2431
3019
  model: request.model,
@@ -2449,35 +3037,96 @@ var OllamaAdapter = class extends AdapterBase {
2449
3037
  }
2450
3038
  async *runStream(providerRequest, factory, request) {
2451
3039
  const auxiliary = this.createAuxiliaryState(request);
3040
+ let completedEmitted = false;
2452
3041
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
2453
3042
  const headers = { "Content-Type": "application/json" };
2454
3043
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
2455
- const response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
2456
- method: "POST",
2457
- headers,
2458
- body: JSON.stringify(providerRequest)
2459
- });
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
+ }
2460
3054
  if (!response.ok) {
2461
- const errorText = await response.text().catch(() => "unknown error");
2462
- throw new Error(`Ollama API error ${response.status}: ${errorText}`);
3055
+ const errorBody = await response.text().catch(() => "");
3056
+ throw providerHttpError(response.status, errorBody);
2463
3057
  }
2464
3058
  const reader = response.body?.getReader();
2465
- 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
+ });
2466
3074
  const output = [];
2467
- const decoder = new TextDecoder();
2468
- let buffer = "";
3075
+ let streamDone = false;
2469
3076
  let responseId;
2470
3077
  let accumulatedContent = "";
2471
3078
  let currentMessageId = "";
2472
3079
  let hasMessageStarted = false;
2473
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
+ };
2474
3124
  try {
2475
3125
  while (true) {
2476
- const { done, value } = await reader.read();
2477
- if (done) break;
2478
- buffer += decoder.decode(value, { stream: true });
2479
- const { chunks, rest, malformedLines } = parseOllamaNDJSON(buffer);
2480
- 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);
2481
3130
  const malformedWarning = emitMalformedStreamWarning(factory, {
2482
3131
  count: malformedLines,
2483
3132
  providerLabel: "Ollama",
@@ -2486,6 +3135,10 @@ var OllamaAdapter = class extends AdapterBase {
2486
3135
  if (malformedWarning) yield malformedWarning;
2487
3136
  for (const chunk of chunks) {
2488
3137
  responseId = chunk.created_at;
3138
+ if (completedEmitted) {
3139
+ if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3140
+ continue;
3141
+ }
2489
3142
  const msg = chunk.message;
2490
3143
  if (msg.content) {
2491
3144
  if (!hasMessageStarted) {
@@ -2494,10 +3147,10 @@ var OllamaAdapter = class extends AdapterBase {
2494
3147
  yield factory.messageStarted(currentMessageId);
2495
3148
  }
2496
3149
  accumulatedContent += msg.content;
2497
- yield factory.messageDelta(currentMessageId, msg.content);
3150
+ yield factory.messageDelta(currentMessageId, textBlock(msg.content));
2498
3151
  }
2499
3152
  if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
2500
- const tcId = `tc-${chunk.created_at}-${tc.function.name}`;
3153
+ const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
2501
3154
  const argsText = JSON.stringify(tc.function.arguments);
2502
3155
  pendingToolCalls.push({
2503
3156
  id: tcId,
@@ -2514,14 +3167,15 @@ var OllamaAdapter = class extends AdapterBase {
2514
3167
  }
2515
3168
  if (hasMessageStarted) {
2516
3169
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2517
- yield factory.messageCompleted(message);
3170
+ yield factory.messageCompleted(currentMessageId);
2518
3171
  if (accumulatedContent) output.push(message);
2519
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);
2520
3174
  for (const pending of pendingToolCalls) {
2521
3175
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2522
3176
  yield factory.toolCallStarted(pending.id, pending.name);
2523
3177
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2524
- yield factory.toolCallCompleted(toolCall);
3178
+ yield factory.toolCallCompleted(pending.id);
2525
3179
  output.push(toolCall);
2526
3180
  }
2527
3181
  if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
@@ -2531,67 +3185,42 @@ var OllamaAdapter = class extends AdapterBase {
2531
3185
  prompt_eval_count: chunk.prompt_eval_count,
2532
3186
  eval_count: chunk.eval_count
2533
3187
  });
2534
- const stopReason = chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0;
2535
- const replay = replayFromOutput(output);
2536
- if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
2537
- role: "assistant",
2538
- content: accumulatedContent,
2539
- tool_calls: pendingToolCalls.map((tc) => ({ function: {
2540
- name: tc.name,
2541
- arguments: tc.argumentsJson
2542
- } }))
2543
- }));
2544
- const auxiliaryResult = await auxiliary.finalize(factory);
2545
- for (const event of auxiliaryResult.events) yield event;
2546
- yield factory.responseCompleted(this.buildResponse(request, {
2547
- output,
2548
- replay,
2549
- stopReason,
2550
- usage: auxiliaryResult.usage,
2551
- billing: auxiliaryResult.billing,
2552
- auxiliary: auxiliaryResult.auxiliary,
2553
- warnings: auxiliaryResult.warnings,
2554
- metadataSources: auxiliaryResult.metadataSources,
2555
- rawResponseId: chunk.created_at
2556
- }, factory));
3188
+ yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
2557
3189
  accumulatedContent = "";
2558
3190
  currentMessageId = "";
2559
3191
  hasMessageStarted = false;
2560
3192
  pendingToolCalls = [];
2561
3193
  }
2562
3194
  }
3195
+ if (done) {
3196
+ streamDone = true;
3197
+ break;
3198
+ }
2563
3199
  }
2564
3200
  } finally {
2565
- reader.releaseLock();
3201
+ try {
3202
+ if (!streamDone) await reader.cancel().catch(() => void 0);
3203
+ } finally {
3204
+ reader.releaseLock();
3205
+ }
2566
3206
  }
2567
- if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
2568
- 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)) {
2569
3209
  yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
2570
3210
  if (hasMessageStarted) {
2571
3211
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
2572
- yield factory.messageCompleted(message);
3212
+ yield factory.messageCompleted(currentMessageId);
2573
3213
  if (accumulatedContent) output.push(message);
2574
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);
2575
3216
  for (const pending of pendingToolCalls) {
2576
3217
  const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
2577
3218
  yield factory.toolCallStarted(pending.id, pending.name);
2578
3219
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
2579
- yield factory.toolCallCompleted(toolCall);
3220
+ yield factory.toolCallCompleted(pending.id);
2580
3221
  output.push(toolCall);
2581
3222
  }
2582
- const replay = replayFromOutput(output);
2583
- const auxiliaryResult = await auxiliary.finalize(factory);
2584
- for (const event of auxiliaryResult.events) yield event;
2585
- yield factory.responseCompleted(this.buildResponse(request, {
2586
- output,
2587
- replay,
2588
- usage: auxiliaryResult.usage,
2589
- billing: auxiliaryResult.billing,
2590
- auxiliary: auxiliaryResult.auxiliary,
2591
- warnings: auxiliaryResult.warnings,
2592
- metadataSources: auxiliaryResult.metadataSources,
2593
- rawResponseId: responseId
2594
- }, factory));
3223
+ yield* emitCompleted(void 0, responseId);
2595
3224
  }
2596
3225
  }
2597
3226
  };
@@ -2626,7 +3255,18 @@ function assertMockRequest(request, expectation, context) {
2626
3255
  }
2627
3256
  var MockAdapter = class extends AdapterBase {
2628
3257
  kind = "mock";
2629
- 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
+ };
2630
3270
  handler;
2631
3271
  providerMetadata;
2632
3272
  cursor = 0;
@@ -2698,18 +3338,34 @@ var MockAdapter = class extends AdapterBase {
2698
3338
  break;
2699
3339
  }
2700
3340
  case "complete": {
2701
- const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
2702
- 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
+ });
2703
3351
  return;
2704
3352
  }
2705
3353
  case "error": {
2706
3354
  yield factory.responseWarning(step.message, step.code);
2707
- const response = this.finalizeTurn(request, factory, mockRequest, output, {
3355
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, {
2708
3356
  type: "complete",
2709
3357
  stopReason: step.stopReason ?? "error",
2710
3358
  providerMetadata: step.providerMetadata
2711
3359
  }, stepCount);
2712
- 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
+ });
2713
3369
  return;
2714
3370
  }
2715
3371
  case "interrupt":
@@ -2718,8 +3374,16 @@ var MockAdapter = class extends AdapterBase {
2718
3374
  case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2719
3375
  }
2720
3376
  }
2721
- const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
2722
- 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
+ });
2723
3387
  } finally {
2724
3388
  this.activeStream = false;
2725
3389
  }
@@ -2847,10 +3511,11 @@ async function* emitMessage(factory, item, stream) {
2847
3511
  let chunkIndex = 0;
2848
3512
  for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
2849
3513
  await delayForChunk(stream, chunkIndex, chunk.length);
2850
- yield factory.messageDelta(item.id, chunk);
3514
+ yield factory.messageDelta(item.id, textBlock(chunk));
2851
3515
  chunkIndex += 1;
2852
3516
  }
2853
- yield factory.messageCompleted(item);
3517
+ else yield factory.messageDelta(item.id, block);
3518
+ yield factory.messageCompleted(item.id);
2854
3519
  }
2855
3520
  async function* emitReasoning(factory, item, stream) {
2856
3521
  if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
@@ -2867,7 +3532,7 @@ async function* emitReasoning(factory, item, stream) {
2867
3532
  chunkIndex += 1;
2868
3533
  }
2869
3534
  }
2870
- yield factory.reasoningCompleted(item);
3535
+ yield factory.reasoningCompleted(item.id);
2871
3536
  }
2872
3537
  async function* emitToolCall(factory, item, streamArguments, stream) {
2873
3538
  yield factory.toolCallStarted(item.id, item.name);
@@ -2879,7 +3544,7 @@ async function* emitToolCall(factory, item, streamArguments, stream) {
2879
3544
  chunkIndex += 1;
2880
3545
  }
2881
3546
  }
2882
- yield factory.toolCallCompleted(item);
3547
+ yield factory.toolCallCompleted(item.id);
2883
3548
  }
2884
3549
  function resolveStepStreamOptions(defaults, override, label) {
2885
3550
  if (override === false) return;
@@ -2990,108 +3655,6 @@ function cloneItem(item) {
2990
3655
  return structuredClone(item);
2991
3656
  }
2992
3657
  //#endregion
2993
- //#region src/helpers/synthetic-stream.ts
2994
- /**
2995
- * 模拟流式 (Synthetic Streaming)
2996
- *
2997
- * 将一组已解析的 canonical OutputItem 包装为规范事件流。
2998
- * 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
2999
- * 即可产出一致的事件序列,无需自己逐事件组装。
3000
- *
3001
- * 约束:
3002
- * - 每个 item 只发一块完整 delta(不模拟逐 token)
3003
- * - 保持 item 边界
3004
- * - 保持后端原始顺序
3005
- * - 不发明 reasoning
3006
- * - 不改写工具参数
3007
- */
3008
- /**
3009
- * 将已解析的 output items 包装为完整规范事件流。
3010
- *
3011
- * 用法示例(在 adapter 的 runStream 中):
3012
- * ```ts
3013
- * const result = parseNonStreamingResponse(data);
3014
- * yield* syntheticStream({
3015
- * model: request.model,
3016
- * responseId: request.requestId,
3017
- * backend: { kind: "chat-completions" },
3018
- * output: result.output,
3019
- * stopReason: result.stopReason,
3020
- * usage: result.usage,
3021
- * });
3022
- * ```
3023
- */
3024
- async function* syntheticStream(options) {
3025
- const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
3026
- const factory = createEventFactory({
3027
- responseId,
3028
- backend: {
3029
- kind: backend.kind,
3030
- isSynthetic: true
3031
- }
3032
- });
3033
- yield factory.responseStarted(model);
3034
- for (const item of output) yield* emitItemEvents(item, factory);
3035
- if (usage || billing) yield factory.responseAuxiliary({
3036
- usage,
3037
- billing
3038
- });
3039
- const finalReplay = replay ?? replayFromOutput(output);
3040
- const allWarnings = [];
3041
- allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
3042
- if (extraWarnings) allWarnings.push(...extraWarnings);
3043
- const response = {
3044
- id: responseId,
3045
- output,
3046
- replay: finalReplay,
3047
- text: extractText(output),
3048
- toolCalls: output.filter((item) => item.type === "tool_call"),
3049
- stopReason,
3050
- usage,
3051
- billing,
3052
- auxiliary: providerMetadata ? { providerMetadata } : void 0,
3053
- warnings: allWarnings.length > 0 ? allWarnings : void 0,
3054
- backend: {
3055
- requestId: responseId,
3056
- rawResponseId,
3057
- adapter: backend.kind,
3058
- isSyntheticStream: true
3059
- }
3060
- };
3061
- yield factory.responseCompleted(response);
3062
- }
3063
- function* emitItemEvents(item, factory) {
3064
- switch (item.type) {
3065
- case "message":
3066
- yield* emitMessageEvents(item, factory);
3067
- break;
3068
- case "reasoning":
3069
- yield* emitReasoningEvents(item, factory);
3070
- break;
3071
- case "tool_call":
3072
- yield* emitToolCallEvents(item, factory);
3073
- break;
3074
- case "opaque": break;
3075
- }
3076
- }
3077
- function* emitMessageEvents(item, factory) {
3078
- const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
3079
- yield factory.messageStarted(id);
3080
- for (const block of item.content) if (block.type === "text") yield factory.messageDelta(id, block.text);
3081
- yield factory.messageCompleted(item);
3082
- }
3083
- function* emitReasoningEvents(item, factory) {
3084
- const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
3085
- yield factory.reasoningStarted(id, item.visibility);
3086
- for (const block of item.content) if (block.type === "text") yield factory.reasoningDelta(id, block);
3087
- yield factory.reasoningCompleted(item);
3088
- }
3089
- function* emitToolCallEvents(item, factory) {
3090
- yield factory.toolCallStarted(item.id, item.name);
3091
- if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
3092
- yield factory.toolCallCompleted(item);
3093
- }
3094
- //#endregion
3095
- 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 };
3096
3659
 
3097
3660
  //# sourceMappingURL=index.mjs.map