@codehz/ai 0.1.1 → 0.1.3

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
@@ -1,55 +1,3 @@
1
- //#region src/types/adapter.ts
2
- const CAPABILITY_MATRIX = {
3
- responses: {
4
- nativeStreaming: true,
5
- messageStreaming: true,
6
- reasoningStreaming: true,
7
- toolCallStreaming: true,
8
- hiddenReasoningReplay: "full",
9
- replayFidelity: "high",
10
- tools: true,
11
- usage: "full",
12
- billing: "lookup",
13
- providerMetadata: true
14
- },
15
- messages: {
16
- nativeStreaming: true,
17
- messageStreaming: true,
18
- reasoningStreaming: false,
19
- toolCallStreaming: true,
20
- hiddenReasoningReplay: "partial",
21
- replayFidelity: "medium",
22
- tools: true,
23
- usage: "full",
24
- billing: "lookup",
25
- providerMetadata: true
26
- },
27
- "chat.completions": {
28
- nativeStreaming: true,
29
- messageStreaming: true,
30
- reasoningStreaming: false,
31
- toolCallStreaming: false,
32
- hiddenReasoningReplay: "none",
33
- replayFidelity: "low",
34
- tools: true,
35
- usage: "full",
36
- billing: "derived",
37
- providerMetadata: false
38
- },
39
- ollama: {
40
- nativeStreaming: true,
41
- messageStreaming: true,
42
- reasoningStreaming: false,
43
- toolCallStreaming: false,
44
- hiddenReasoningReplay: "none",
45
- replayFidelity: "low",
46
- tools: true,
47
- usage: "partial",
48
- billing: "none",
49
- providerMetadata: false
50
- }
51
- };
52
- //#endregion
53
1
  //#region src/core/errors.ts
54
2
  var AIError = class extends Error {
55
3
  code;
@@ -491,17 +439,14 @@ function createEventFactory(state) {
491
439
  }
492
440
  //#endregion
493
441
  //#region src/core/aggregator.ts
494
- function createInitialState() {
442
+ function createAggregatorState() {
495
443
  return {
496
- pendingMessages: /* @__PURE__ */ new Map(),
497
- pendingReasonings: /* @__PURE__ */ new Map(),
498
- pendingToolCalls: /* @__PURE__ */ new Map(),
499
- outputOrder: [],
500
- completedMessages: /* @__PURE__ */ new Map(),
501
- completedReasonings: /* @__PURE__ */ new Map(),
502
- completedToolCalls: /* @__PURE__ */ new Map(),
503
444
  auxiliary: {},
504
- warnings: []
445
+ warnings: [],
446
+ warningSet: /* @__PURE__ */ new Set(),
447
+ output: [],
448
+ textParts: [],
449
+ toolCalls: []
505
450
  };
506
451
  }
507
452
  function handleResponseStarted(state, event) {
@@ -523,55 +468,16 @@ function handleResponseAuxiliary(state, event) {
523
468
  };
524
469
  if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
525
470
  }
526
- function handleMessageStarted(state, event) {
527
- state.pendingMessages.set(event.item.id, {
528
- role: event.item.role,
529
- texts: []
530
- });
531
- }
532
- function handleMessageDelta(state, event) {
533
- const pending = state.pendingMessages.get(event.itemId);
534
- if (pending) pending.texts.push(event.delta.text);
535
- }
536
471
  function handleMessageCompleted(state, event) {
537
- const item = event.item;
538
- const itemId = item.id ?? `msg-${state.outputOrder.length}`;
539
- state.completedMessages.set(itemId, item);
540
- state.outputOrder.push(itemId);
541
- state.pendingMessages.delete(itemId);
542
- }
543
- function handleReasoningStarted(state, event) {
544
- state.pendingReasonings.set(event.item.id, {
545
- visibility: event.item.visibility,
546
- blocks: []
547
- });
548
- }
549
- function handleReasoningDelta(state, event) {
550
- const pending = state.pendingReasonings.get(event.itemId);
551
- if (pending) pending.blocks.push(event.delta);
472
+ state.output.push(event.item);
473
+ pushMessageText(state, event.item);
552
474
  }
553
475
  function handleReasoningCompleted(state, event) {
554
- const item = event.item;
555
- const stableId = item.id ?? `reason-${state.outputOrder.length}-${Date.now()}`;
556
- state.completedReasonings.set(stableId, item);
557
- state.outputOrder.push(stableId);
558
- state.pendingReasonings.delete(item.id ?? "");
559
- }
560
- function handleToolCallStarted(state, event) {
561
- state.pendingToolCalls.set(event.item.id, {
562
- name: event.item.name,
563
- argsParts: []
564
- });
565
- }
566
- function handleToolCallDelta(state, event) {
567
- const pending = state.pendingToolCalls.get(event.itemId);
568
- if (pending && event.delta.argumentsText) pending.argsParts.push(event.delta.argumentsText);
476
+ state.output.push(event.item);
569
477
  }
570
478
  function handleToolCallCompleted(state, event) {
571
- const item = event.item;
572
- state.completedToolCalls.set(item.id, item);
573
- state.outputOrder.push(item.id);
574
- state.pendingToolCalls.delete(item.id);
479
+ state.output.push(event.item);
480
+ state.toolCalls.push(event.item);
575
481
  }
576
482
  function handleResponseCompleted(state, event) {
577
483
  state.replayFromAdapter = event.response.replay;
@@ -590,26 +496,6 @@ function handleResponseCompleted(state, event) {
590
496
  if (event.response.warnings) pushWarnings(state, event.response.warnings);
591
497
  }
592
498
  function buildResponse(state) {
593
- const output = [];
594
- for (const id of state.outputOrder) {
595
- const msg = state.completedMessages.get(id);
596
- if (msg) {
597
- output.push(msg);
598
- continue;
599
- }
600
- const reason = state.completedReasonings.get(id);
601
- if (reason) {
602
- output.push(reason);
603
- continue;
604
- }
605
- const tc = state.completedToolCalls.get(id);
606
- if (tc) {
607
- output.push(tc);
608
- continue;
609
- }
610
- }
611
- const text = output.filter((item) => item.type === "message").flatMap((m) => m.content).filter((b) => b.type === "text").map((b) => b.text).join("");
612
- const toolCalls = output.filter((item) => item.type === "tool_call");
613
499
  const backendFromResponse = state.backendFromAdapter;
614
500
  const backend = {
615
501
  adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? "unknown",
@@ -621,10 +507,10 @@ function buildResponse(state) {
621
507
  };
622
508
  return {
623
509
  id: state.responseIdFromAdapter ?? state.responseId,
624
- output,
510
+ output: state.output,
625
511
  replay: state.replayFromAdapter ?? [],
626
- text,
627
- toolCalls,
512
+ text: state.textParts.join(""),
513
+ toolCalls: state.toolCalls,
628
514
  stopReason: state.stopReasonFromAdapter,
629
515
  usage: state.usage,
630
516
  billing: state.billing,
@@ -638,8 +524,13 @@ function buildResponse(state) {
638
524
  * 适用于测试和离线处理场景。
639
525
  */
640
526
  function aggregateEvents(events) {
641
- const state = createInitialState();
642
- for (const event of events) switch (event.type) {
527
+ const state = createAggregatorState();
528
+ for (const event of events) aggregateEvent(state, event);
529
+ return finalizeAggregation(state);
530
+ }
531
+ function aggregateEvent(state, event) {
532
+ state.lastEventType = event.type;
533
+ switch (event.type) {
643
534
  case "response.started":
644
535
  handleResponseStarted(state, event);
645
536
  break;
@@ -650,29 +541,17 @@ function aggregateEvents(events) {
650
541
  handleResponseAuxiliary(state, event);
651
542
  break;
652
543
  case "message.started":
653
- handleMessageStarted(state, event);
654
- break;
655
544
  case "message.delta":
656
- handleMessageDelta(state, event);
657
- break;
658
- case "message.completed":
659
- handleMessageCompleted(state, event);
660
- break;
661
545
  case "reasoning.started":
662
- handleReasoningStarted(state, event);
663
- break;
664
546
  case "reasoning.delta":
665
- handleReasoningDelta(state, event);
547
+ case "tool_call.started":
548
+ case "tool_call.delta": break;
549
+ case "message.completed":
550
+ handleMessageCompleted(state, event);
666
551
  break;
667
552
  case "reasoning.completed":
668
553
  handleReasoningCompleted(state, event);
669
554
  break;
670
- case "tool_call.started":
671
- handleToolCallStarted(state, event);
672
- break;
673
- case "tool_call.delta":
674
- handleToolCallDelta(state, event);
675
- break;
676
555
  case "tool_call.completed":
677
556
  handleToolCallCompleted(state, event);
678
557
  break;
@@ -680,8 +559,9 @@ function aggregateEvents(events) {
680
559
  handleResponseCompleted(state, event);
681
560
  break;
682
561
  }
683
- const lastEvent = events[events.length - 1];
684
- if (!lastEvent || lastEvent.type !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
562
+ }
563
+ function finalizeAggregation(state) {
564
+ if (state.lastEventType !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
685
565
  return buildResponse(state);
686
566
  }
687
567
  function mergeAuxiliary$1(base, patch) {
@@ -690,20 +570,26 @@ function mergeAuxiliary$1(base, patch) {
690
570
  ...patch
691
571
  };
692
572
  if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
693
- ...base.providerMetadata ?? {},
694
- ...patch.providerMetadata ?? {}
573
+ ...base.providerMetadata,
574
+ ...patch.providerMetadata
695
575
  };
696
576
  return merged;
697
577
  }
698
578
  function pushWarnings(state, warnings) {
699
- for (const warning of warnings) if (!state.warnings.includes(warning)) state.warnings.push(warning);
579
+ for (const warning of warnings) if (!state.warningSet.has(warning)) {
580
+ state.warningSet.add(warning);
581
+ state.warnings.push(warning);
582
+ }
583
+ }
584
+ function pushMessageText(state, item) {
585
+ for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
700
586
  }
701
587
  //#endregion
702
588
  //#region src/core/collect-stream.ts
703
589
  async function collectStream(stream) {
704
- const events = [];
705
- for await (const event of stream) events.push(event);
706
- return aggregateEvents(events);
590
+ const state = createAggregatorState();
591
+ for await (const event of stream) aggregateEvent(state, event);
592
+ return finalizeAggregation(state);
707
593
  }
708
594
  //#endregion
709
595
  //#region src/helpers/mapping.ts
@@ -951,12 +837,10 @@ function withTimeout(promise, ms) {
951
837
  //#region src/helpers/adapter-auxiliary.ts
952
838
  var AdapterAuxiliaryState = class {
953
839
  request;
954
- capabilities;
955
840
  collector = new AuxiliaryCollector();
956
841
  metadataSources = /* @__PURE__ */ new Set();
957
- constructor(request, capabilities) {
842
+ constructor(request) {
958
843
  this.request = request;
959
- this.capabilities = capabilities;
960
844
  }
961
845
  recordUsage(usage, source, raw) {
962
846
  if (this.request.include?.usage === "off" || isEmptyRecord(usage)) return;
@@ -980,8 +864,7 @@ var AdapterAuxiliaryState = class {
980
864
  request: this.request,
981
865
  usage: snapshot.usage,
982
866
  billing: snapshot.billing,
983
- auxiliary: snapshot.auxiliary,
984
- capabilities: this.capabilities
867
+ auxiliary: snapshot.auxiliary
985
868
  });
986
869
  if (derived && !isEmptyRecord(derived)) this.collector.recordBilling({
987
870
  ...derived,
@@ -1046,7 +929,7 @@ var AdapterBase = class {
1046
929
  responseId: request.requestId,
1047
930
  backend: {
1048
931
  kind: this.kind,
1049
- isSynthetic: !this.capabilities.nativeStreaming
932
+ isSynthetic: !this.nativeStreaming
1050
933
  }
1051
934
  });
1052
935
  yield factory.responseStarted(request.model);
@@ -1085,7 +968,7 @@ var AdapterBase = class {
1085
968
  requestId: request.requestId,
1086
969
  rawResponseId: result.rawResponseId,
1087
970
  adapter: this.kind,
1088
- isSyntheticStream: !this.capabilities.nativeStreaming,
971
+ isSyntheticStream: !this.nativeStreaming,
1089
972
  metadataSources: result.metadataSources,
1090
973
  warnings
1091
974
  }
@@ -1096,18 +979,18 @@ var AdapterBase = class {
1096
979
  return extractText(output);
1097
980
  }
1098
981
  createAuxiliaryState(request) {
1099
- return new AdapterAuxiliaryState(request, this.capabilities);
982
+ return new AdapterAuxiliaryState(request);
1100
983
  }
1101
984
  };
1102
985
  function mergeAuxiliary(base, patch) {
1103
986
  if (!base && !patch) return void 0;
1104
987
  const merged = {
1105
- ...base ?? {},
1106
- ...patch ?? {}
988
+ ...base,
989
+ ...patch
1107
990
  };
1108
991
  if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
1109
- ...base?.providerMetadata ?? {},
1110
- ...patch?.providerMetadata ?? {}
992
+ ...base?.providerMetadata,
993
+ ...patch?.providerMetadata
1111
994
  };
1112
995
  return merged;
1113
996
  }
@@ -1189,6 +1072,7 @@ function parseSSEEvents(chunk) {
1189
1072
  function ensureResponsesTextBlocks(blocks, field) {
1190
1073
  for (let i = 0; i < blocks.length; i++) {
1191
1074
  const block = blocks[i];
1075
+ if (!block) continue;
1192
1076
  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");
1193
1077
  }
1194
1078
  return blocks;
@@ -1236,7 +1120,7 @@ function canonicalToResponsesBlock(b) {
1236
1120
  }
1237
1121
  var ResponsesAdapter = class extends AdapterBase {
1238
1122
  kind = "responses";
1239
- capabilities = CAPABILITY_MATRIX.responses;
1123
+ nativeStreaming = true;
1240
1124
  apiKey;
1241
1125
  baseUrl;
1242
1126
  fetchFn;
@@ -1468,6 +1352,7 @@ var ResponsesAdapter = class extends AdapterBase {
1468
1352
  function ensureMessagesTextBlocks(blocks, field) {
1469
1353
  for (let i = 0; i < blocks.length; i++) {
1470
1354
  const block = blocks[i];
1355
+ if (!block) continue;
1471
1356
  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");
1472
1357
  }
1473
1358
  return blocks;
@@ -1539,7 +1424,7 @@ function buildStreamMetadata(options) {
1539
1424
  }
1540
1425
  var MessagesAdapter = class extends AdapterBase {
1541
1426
  kind = "messages";
1542
- capabilities = CAPABILITY_MATRIX.messages;
1427
+ nativeStreaming = true;
1543
1428
  apiKey;
1544
1429
  apiVersion;
1545
1430
  baseUrl;
@@ -1932,6 +1817,7 @@ function parseChatSSE(buffer) {
1932
1817
  function ensureTextCompatibleBlocks(blocks, field) {
1933
1818
  for (let i = 0; i < blocks.length; i++) {
1934
1819
  const block = blocks[i];
1820
+ if (!block) continue;
1935
1821
  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");
1936
1822
  }
1937
1823
  return blocks;
@@ -1992,26 +1878,10 @@ function buildAssistantReplayMessage(params) {
1992
1878
  }
1993
1879
  var ChatCompletionsAdapter = class extends AdapterBase {
1994
1880
  kind = "chat-completions";
1995
- capabilities = {
1996
- nativeStreaming: true,
1997
- messageStreaming: true,
1998
- reasoningStreaming: false,
1999
- toolCallStreaming: false,
2000
- hiddenReasoningReplay: "none",
2001
- replayFidelity: "low",
2002
- tools: true,
2003
- usage: "full",
2004
- billing: "derived",
2005
- providerMetadata: false
2006
- };
1881
+ nativeStreaming = true;
2007
1882
  apiKey;
2008
1883
  baseUrl;
2009
1884
  fetchFn;
2010
- markReasoningCompatibility() {
2011
- this.capabilities.reasoningStreaming = true;
2012
- this.capabilities.hiddenReasoningReplay = "partial";
2013
- this.capabilities.replayFidelity = "medium";
2014
- }
2015
1885
  constructor(options) {
2016
1886
  super();
2017
1887
  this.apiKey = options.apiKey;
@@ -2136,7 +2006,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2136
2006
  let currentReasoningId = "";
2137
2007
  let hasMessageStarted = false;
2138
2008
  let hasReasoningStarted = false;
2139
- let hasStreamedReasoning = false;
2140
2009
  const pendingToolCalls = /* @__PURE__ */ new Map();
2141
2010
  const reasoningByField = /* @__PURE__ */ new Map();
2142
2011
  const finalizePendingTurn = () => {
@@ -2211,7 +2080,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2211
2080
  if (!hasReasoningStarted) {
2212
2081
  currentReasoningId = `reason-${chunk.id}`;
2213
2082
  hasReasoningStarted = true;
2214
- hasStreamedReasoning = true;
2215
2083
  accumulatedReasoning = "";
2216
2084
  yield factory.reasoningStarted(currentReasoningId, "full");
2217
2085
  }
@@ -2269,7 +2137,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2269
2137
  if (finishReason && finishReason !== null) {
2270
2138
  const { events, assistantReplayMessage } = finalizePendingTurn();
2271
2139
  for (const event of events) yield event;
2272
- if (hasStreamedReasoning) this.markReasoningCompatibility();
2273
2140
  const stopReason = mapStopReason(finishReason);
2274
2141
  const replay = [...replayFromOutput(output)];
2275
2142
  if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
@@ -2299,7 +2166,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2299
2166
  if (buffer.trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2300
2167
  if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
2301
2168
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2302
- if (hasStreamedReasoning) this.markReasoningCompatibility();
2303
2169
  const { events, assistantReplayMessage } = finalizePendingTurn();
2304
2170
  for (const event of events) yield event;
2305
2171
  const replay = [...replayFromOutput(output)];
@@ -2343,6 +2209,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2343
2209
  function ensureOllamaTextBlocks(blocks, field) {
2344
2210
  for (let i = 0; i < blocks.length; i++) {
2345
2211
  const block = blocks[i];
2212
+ if (!block) continue;
2346
2213
  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");
2347
2214
  }
2348
2215
  return blocks;
@@ -2403,18 +2270,7 @@ function isOllamaToolCalls(value) {
2403
2270
  }
2404
2271
  var OllamaAdapter = class extends AdapterBase {
2405
2272
  kind = "ollama";
2406
- capabilities = {
2407
- nativeStreaming: true,
2408
- messageStreaming: true,
2409
- reasoningStreaming: false,
2410
- toolCallStreaming: false,
2411
- hiddenReasoningReplay: "none",
2412
- replayFidelity: "low",
2413
- tools: true,
2414
- usage: "partial",
2415
- billing: "none",
2416
- providerMetadata: false
2417
- };
2273
+ nativeStreaming = true;
2418
2274
  baseUrl;
2419
2275
  apiKey;
2420
2276
  fetchFn;
@@ -2651,6 +2507,390 @@ var OllamaAdapter = class extends AdapterBase {
2651
2507
  }
2652
2508
  };
2653
2509
  //#endregion
2510
+ //#region src/adapters/mock.ts
2511
+ /**
2512
+ * Mock Adapter
2513
+ *
2514
+ * 面向测试的脚本化 adapter:
2515
+ * - 按 turn 顺序消费请求,验证调用方是否正确续接 replay / tool_result
2516
+ * - 发出可控的 message / reasoning / tool_call 流
2517
+ * - 注入 warning / auxiliary / content_filter / 中断 / provider error
2518
+ *
2519
+ * 这不是通用“假模型”,而是测试工具调用编排与错误路径的测试夹具。
2520
+ */
2521
+ var MockAdapter = class extends AdapterBase {
2522
+ kind = "mock";
2523
+ nativeStreaming = false;
2524
+ turns;
2525
+ onExhausted;
2526
+ providerMetadata;
2527
+ defaultStream;
2528
+ cursor = 0;
2529
+ previousReplay = [];
2530
+ pendingToolCalls = [];
2531
+ history = [];
2532
+ activeStream = false;
2533
+ constructor(options) {
2534
+ super();
2535
+ this.turns = options.turns;
2536
+ this.onExhausted = options.onExhausted ?? "throw";
2537
+ this.providerMetadata = options.providerMetadata;
2538
+ this.defaultStream = resolveMockTextStreamOptions(options.stream, "adapter stream");
2539
+ }
2540
+ async buildRequest(request) {
2541
+ const turnIndex = this.cursor;
2542
+ const turn = this.resolveTurn(turnIndex);
2543
+ const turnName = turn.name;
2544
+ const context = this.buildTurnContext(turnIndex);
2545
+ if (turn.expect) if (typeof turn.expect === "function") await turn.expect(request, context);
2546
+ else assertRequestMatchesExpectation(request, turn.expect, context);
2547
+ const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
2548
+ this.cursor += 1;
2549
+ return {
2550
+ request,
2551
+ turn,
2552
+ turnIndex,
2553
+ turnName,
2554
+ remainingPendingToolCalls
2555
+ };
2556
+ }
2557
+ async *runStream(providerRequest, factory, request) {
2558
+ if (this.activeStream) throw new AIRequestError("MockAdapter does not support concurrent streams", "MOCK_CONCURRENT_STREAM");
2559
+ this.activeStream = true;
2560
+ try {
2561
+ const mockRequest = providerRequest;
2562
+ const output = [];
2563
+ for (const [stepIndex, step] of mockRequest.turn.steps.entries()) switch (step.type) {
2564
+ case "warning":
2565
+ yield factory.responseWarning(step.message, step.code);
2566
+ break;
2567
+ case "auxiliary":
2568
+ yield factory.responseAuxiliary({
2569
+ usage: step.usage,
2570
+ billing: step.billing,
2571
+ auxiliary: step.auxiliary
2572
+ });
2573
+ break;
2574
+ case "message": {
2575
+ const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepIndex);
2576
+ yield* emitMessage(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "message"));
2577
+ output.push(item);
2578
+ break;
2579
+ }
2580
+ case "reasoning": {
2581
+ const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepIndex);
2582
+ yield* emitReasoning(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "reasoning"));
2583
+ output.push(item);
2584
+ break;
2585
+ }
2586
+ case "tool_call": {
2587
+ const item = createToolCallFromStep(step);
2588
+ yield* emitToolCall(factory, item, step.streamArguments ?? true, resolveStepStreamOptions(this.defaultStream, step.stream, "tool_call"));
2589
+ output.push(item);
2590
+ break;
2591
+ }
2592
+ case "output": {
2593
+ assertSupportedOutputItem(step.item);
2594
+ const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepIndex);
2595
+ yield* emitOutputItem(factory, item, resolveStepStreamOptions(this.defaultStream, step.stream, "output"));
2596
+ output.push(item);
2597
+ break;
2598
+ }
2599
+ case "complete": {
2600
+ const response = this.finalizeTurn(request, factory, mockRequest, output, step);
2601
+ yield factory.responseCompleted(response);
2602
+ return;
2603
+ }
2604
+ case "error": {
2605
+ yield factory.responseWarning(step.message, step.code);
2606
+ const response = this.finalizeTurn(request, factory, mockRequest, output, {
2607
+ type: "complete",
2608
+ stopReason: step.stopReason ?? "error",
2609
+ providerMetadata: step.providerMetadata
2610
+ });
2611
+ yield factory.responseCompleted(response);
2612
+ return;
2613
+ }
2614
+ case "interrupt":
2615
+ this.pendingToolCalls = mockRequest.remainingPendingToolCalls;
2616
+ return;
2617
+ case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2618
+ }
2619
+ const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" });
2620
+ yield factory.responseCompleted(response);
2621
+ } finally {
2622
+ this.activeStream = false;
2623
+ }
2624
+ }
2625
+ finalizeTurn(request, factory, mockRequest, output, completion) {
2626
+ const replay = completion.replay ?? replayFromOutput(output);
2627
+ const toolCalls = output.filter((item) => item.type === "tool_call");
2628
+ this.previousReplay = replay;
2629
+ this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
2630
+ this.history.push({
2631
+ turnIndex: mockRequest.turnIndex,
2632
+ turnName: mockRequest.turnName,
2633
+ requestId: request.requestId,
2634
+ replay,
2635
+ toolCalls
2636
+ });
2637
+ return this.buildResponse(request, {
2638
+ output,
2639
+ replay,
2640
+ stopReason: completion.stopReason ?? resolveStopReason(output),
2641
+ usage: completion.usage,
2642
+ billing: completion.billing,
2643
+ auxiliary: completion.auxiliary,
2644
+ providerMetadata: {
2645
+ turnIndex: mockRequest.turnIndex,
2646
+ turnName: mockRequest.turnName,
2647
+ scriptedSteps: mockRequest.turn.steps.length,
2648
+ pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
2649
+ historyLength: this.history.length,
2650
+ ...this.providerMetadata,
2651
+ ...completion.providerMetadata
2652
+ },
2653
+ warnings: completion.warnings,
2654
+ metadataSources: ["mock"],
2655
+ rawResponseId: completion.rawResponseId
2656
+ }, factory);
2657
+ }
2658
+ resolveTurn(turnIndex) {
2659
+ const turn = this.turns[turnIndex];
2660
+ if (turn !== void 0) return turn;
2661
+ const lastTurn = this.turns.at(-1);
2662
+ if (this.onExhausted === "repeat-last" && lastTurn !== void 0) return lastTurn;
2663
+ if (this.onExhausted === "complete-empty") return {
2664
+ name: "exhausted",
2665
+ steps: []
2666
+ };
2667
+ throw new AIRequestError(`MockAdapter turn ${turnIndex + 1} requested, but only ${this.turns.length} turn(s) were scripted`, "MOCK_TURN_EXHAUSTED");
2668
+ }
2669
+ buildTurnContext(turnIndex) {
2670
+ return {
2671
+ turnIndex,
2672
+ previousReplay: this.previousReplay.map(cloneItem),
2673
+ pendingToolCalls: this.pendingToolCalls.map(cloneItem),
2674
+ history: this.history.map((record) => ({
2675
+ ...record,
2676
+ replay: record.replay.map(cloneItem),
2677
+ toolCalls: record.toolCalls.map(cloneItem)
2678
+ }))
2679
+ };
2680
+ }
2681
+ };
2682
+ function createMessageFromStep(step, request, turnIndex, stepIndex) {
2683
+ return {
2684
+ ...messageItem(normalizeBlocks(step.content), { id: step.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}` }),
2685
+ role: "assistant"
2686
+ };
2687
+ }
2688
+ function createReasoningFromStep(step, request, turnIndex, stepIndex) {
2689
+ return reasoningItem(normalizeBlocks(step.content), step.visibility ?? "full", step.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`);
2690
+ }
2691
+ function createToolCallFromStep(step) {
2692
+ return {
2693
+ type: "tool_call",
2694
+ id: step.id,
2695
+ name: step.name,
2696
+ argumentsText: step.argumentsText,
2697
+ argumentsJson: step.argumentsJson
2698
+ };
2699
+ }
2700
+ function normalizeBlocks(content) {
2701
+ return typeof content === "string" ? [textBlock(content)] : content;
2702
+ }
2703
+ function assertSupportedOutputItem(item) {
2704
+ if (item.type === "opaque") throw new AIRequestError("MockAdapter does not stream opaque output items; use complete.replay if needed", "MOCK_OPAQUE_OUTPUT");
2705
+ }
2706
+ function attachSyntheticId(item, request, turnIndex, stepIndex) {
2707
+ if (item.type === "message") return {
2708
+ ...item,
2709
+ id: item.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}`,
2710
+ role: "assistant"
2711
+ };
2712
+ if (item.type === "reasoning") return {
2713
+ ...item,
2714
+ id: item.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`
2715
+ };
2716
+ return item;
2717
+ }
2718
+ async function* emitOutputItem(factory, item, stream) {
2719
+ if (item.type === "message") {
2720
+ yield* emitMessage(factory, item, stream);
2721
+ return;
2722
+ }
2723
+ if (item.type === "reasoning") {
2724
+ yield* emitReasoning(factory, item, stream);
2725
+ return;
2726
+ }
2727
+ yield* emitToolCall(factory, item, true, stream);
2728
+ }
2729
+ async function* emitMessage(factory, item, stream) {
2730
+ if (!item.id) throw new AIRequestError("Mock message output requires an id after normalization", "MOCK_MESSAGE_ID_MISSING");
2731
+ yield factory.messageStarted(item.id);
2732
+ let chunkIndex = 0;
2733
+ for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
2734
+ await delayForChunk(stream, chunkIndex, chunk.length);
2735
+ yield factory.messageDelta(item.id, chunk);
2736
+ chunkIndex += 1;
2737
+ }
2738
+ yield factory.messageCompleted(item);
2739
+ }
2740
+ async function* emitReasoning(factory, item, stream) {
2741
+ if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
2742
+ yield factory.reasoningStarted(item.id, item.visibility);
2743
+ let chunkIndex = 0;
2744
+ for (const block of item.content) {
2745
+ if (block.type !== "text") {
2746
+ yield factory.reasoningDelta(item.id, block);
2747
+ continue;
2748
+ }
2749
+ for (const chunk of chunkText(block.text, stream)) {
2750
+ await delayForChunk(stream, chunkIndex, chunk.length);
2751
+ yield factory.reasoningDelta(item.id, textBlock(chunk));
2752
+ chunkIndex += 1;
2753
+ }
2754
+ }
2755
+ yield factory.reasoningCompleted(item);
2756
+ }
2757
+ async function* emitToolCall(factory, item, streamArguments, stream) {
2758
+ yield factory.toolCallStarted(item.id, item.name);
2759
+ if (streamArguments && item.argumentsText) {
2760
+ let chunkIndex = 0;
2761
+ for (const chunk of chunkText(item.argumentsText, stream)) {
2762
+ await delayForChunk(stream, chunkIndex, chunk.length);
2763
+ yield factory.toolCallDelta(item.id, { argumentsText: chunk });
2764
+ chunkIndex += 1;
2765
+ }
2766
+ }
2767
+ yield factory.toolCallCompleted(item);
2768
+ }
2769
+ function resolveStepStreamOptions(defaults, override, label) {
2770
+ if (override === false) return;
2771
+ return resolveMockTextStreamOptions(override, `${label} stream`, defaults);
2772
+ }
2773
+ function resolveMockTextStreamOptions(options, label, defaults) {
2774
+ if (options === void 0) return defaults;
2775
+ const chunkSize = options.chunkSize ?? defaults?.chunkSize ?? 1;
2776
+ const initialDelayMs = options.initialDelayMs ?? defaults?.initialDelayMs ?? 0;
2777
+ const charsPerSecond = options.charsPerSecond ?? defaults?.charsPerSecond;
2778
+ if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new AIRequestError(`${label}: chunkSize must be a positive integer`, "MOCK_STREAM_CONFIG_INVALID");
2779
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs < 0) throw new AIRequestError(`${label}: initialDelayMs must be a non-negative number`, "MOCK_STREAM_CONFIG_INVALID");
2780
+ if (charsPerSecond !== void 0 && (!Number.isFinite(charsPerSecond) || charsPerSecond <= 0)) throw new AIRequestError(`${label}: charsPerSecond must be a positive number`, "MOCK_STREAM_CONFIG_INVALID");
2781
+ return {
2782
+ chunkSize,
2783
+ initialDelayMs,
2784
+ charsPerSecond
2785
+ };
2786
+ }
2787
+ function chunkText(text, stream) {
2788
+ if (!text) return [];
2789
+ if (!stream) return [text];
2790
+ const chars = Array.from(text);
2791
+ const chunks = [];
2792
+ for (let index = 0; index < chars.length; index += stream.chunkSize) chunks.push(chars.slice(index, index + stream.chunkSize).join(""));
2793
+ return chunks;
2794
+ }
2795
+ async function delayForChunk(stream, chunkIndex, chunkLength) {
2796
+ if (!stream) return;
2797
+ if (chunkIndex === 0 && stream.initialDelayMs > 0) {
2798
+ await sleep(stream.initialDelayMs);
2799
+ return;
2800
+ }
2801
+ if (chunkIndex > 0 && stream.charsPerSecond !== void 0) await sleep(chunkLength / stream.charsPerSecond * 1e3);
2802
+ }
2803
+ async function sleep(ms) {
2804
+ if (ms <= 0) return;
2805
+ await new Promise((resolve) => setTimeout(resolve, ms));
2806
+ }
2807
+ function resolveStopReason(output) {
2808
+ return output.some((item) => item.type === "tool_call") ? "tool_call" : "end_turn";
2809
+ }
2810
+ function consumePendingToolCalls(pending, input) {
2811
+ const fulfilledIds = new Set(input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2812
+ return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
2813
+ }
2814
+ function assertRequestMatchesExpectation(request, expectation, context) {
2815
+ const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
2816
+ if (expectation.minItems !== void 0 && request.input.length < expectation.minItems) throw new AIRequestError(`${prefix}: expected at least ${expectation.minItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
2817
+ if (expectation.maxItems !== void 0 && request.input.length > expectation.maxItems) throw new AIRequestError(`${prefix}: expected at most ${expectation.maxItems} input item(s)`, "MOCK_EXPECTATION_FAILED");
2818
+ if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
2819
+ if (expectation.tools === "absent" && request.tools && request.tools.length > 0) throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
2820
+ if (expectation.toolChoice === "present" && request.toolChoice === void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
2821
+ if (expectation.toolChoice === "absent" && request.toolChoice !== void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
2822
+ if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) assertReplayIncluded(request.input, context.previousReplay, prefix);
2823
+ if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
2824
+ const toolResultIds = new Set(request.input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2825
+ for (const call of context.pendingToolCalls) if (!toolResultIds.has(call.id)) throw new AIRequestError(`${prefix}: expected tool_result for pending tool call "${call.id}"`, "MOCK_EXPECTATION_FAILED");
2826
+ }
2827
+ if (expectation.items && expectation.items.length > 0) if (expectation.ordered) assertOrderedItems(request.input, expectation.items, prefix);
2828
+ else assertUnorderedItems(request.input, expectation.items, prefix);
2829
+ }
2830
+ function assertReplayIncluded(input, replay, prefix) {
2831
+ const fingerprints = input.map(fingerprintItem);
2832
+ let cursor = 0;
2833
+ for (const replayItem of replay) {
2834
+ const target = fingerprintItem(replayItem);
2835
+ const foundIndex = fingerprints.indexOf(target, cursor);
2836
+ if (foundIndex === -1) throw new AIRequestError(`${prefix}: previous replay item was not carried into the next request`, "MOCK_EXPECTATION_FAILED");
2837
+ cursor = foundIndex + 1;
2838
+ }
2839
+ }
2840
+ function assertOrderedItems(input, expectations, prefix) {
2841
+ let cursor = 0;
2842
+ for (const expected of expectations) {
2843
+ let matched = false;
2844
+ while (cursor < input.length) {
2845
+ const item = input[cursor];
2846
+ if (item !== void 0 && matchesItemExpectation(item, expected)) {
2847
+ matched = true;
2848
+ cursor += 1;
2849
+ break;
2850
+ }
2851
+ cursor += 1;
2852
+ }
2853
+ if (!matched) throw new AIRequestError(`${prefix}: missing ordered input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
2854
+ }
2855
+ }
2856
+ function assertUnorderedItems(input, expectations, prefix) {
2857
+ for (const expected of expectations) if (!input.some((item) => matchesItemExpectation(item, expected))) throw new AIRequestError(`${prefix}: missing input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
2858
+ }
2859
+ function matchesItemExpectation(item, expected) {
2860
+ if (item.type !== expected.type) return false;
2861
+ if (expected.id !== void 0 && "id" in item && item.id !== expected.id) return false;
2862
+ switch (item.type) {
2863
+ case "message": return (expected.role === void 0 || item.role === expected.role) && matchesText(item.content, expected.textIncludes);
2864
+ case "reasoning": return (expected.visibility === void 0 || item.visibility === expected.visibility) && matchesText(item.content, expected.textIncludes);
2865
+ case "tool_call": return (expected.name === void 0 || item.name === expected.name) && (expected.textIncludes === void 0 || item.argumentsText.includes(expected.textIncludes));
2866
+ case "tool_result": return (expected.toolName === void 0 || item.toolName === expected.toolName) && (expected.callId === void 0 || item.callId === expected.callId) && (expected.outcome === void 0 || item.outcome === expected.outcome) && matchesText(item.content, expected.textIncludes);
2867
+ case "opaque": return (expected.source === void 0 || item.source === expected.source) && (expected.purpose === void 0 || item.purpose === expected.purpose);
2868
+ }
2869
+ }
2870
+ function matchesText(blocks, textIncludes) {
2871
+ if (textIncludes === void 0) return true;
2872
+ return blocks.some((block) => {
2873
+ if (block.type === "text") return block.text.includes(textIncludes);
2874
+ if (block.type === "json") return JSON.stringify(block.json).includes(textIncludes);
2875
+ return false;
2876
+ });
2877
+ }
2878
+ function fingerprintItem(item) {
2879
+ return JSON.stringify(item);
2880
+ }
2881
+ function describeExpectation(expectation) {
2882
+ const parts = [`type=${expectation.type}`];
2883
+ if (expectation.role) parts.push(`role=${expectation.role}`);
2884
+ if (expectation.name) parts.push(`name=${expectation.name}`);
2885
+ if (expectation.toolName) parts.push(`toolName=${expectation.toolName}`);
2886
+ if (expectation.callId) parts.push(`callId=${expectation.callId}`);
2887
+ if (expectation.textIncludes) parts.push(`textIncludes=${JSON.stringify(expectation.textIncludes)}`);
2888
+ return `{ ${parts.join(", ")} }`;
2889
+ }
2890
+ function cloneItem(item) {
2891
+ return structuredClone(item);
2892
+ }
2893
+ //#endregion
2654
2894
  //#region src/helpers/synthetic-stream.ts
2655
2895
  /**
2656
2896
  * 模拟流式 (Synthetic Streaming)
@@ -2753,6 +2993,6 @@ function* emitToolCallEvents(item, factory) {
2753
2993
  yield factory.toolCallCompleted(item);
2754
2994
  }
2755
2995
  //#endregion
2756
- export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, CAPABILITY_MATRIX, ChatCompletionsAdapter, MessagesAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, 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, validateRequest };
2996
+ export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, MessagesAdapter, MockAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, 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, validateRequest };
2757
2997
 
2758
2998
  //# sourceMappingURL=index.mjs.map