@codehz/ai 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -47,6 +47,18 @@ const CAPABILITY_MATRIX = {
47
47
  usage: "partial",
48
48
  billing: "none",
49
49
  providerMetadata: false
50
+ },
51
+ mock: {
52
+ nativeStreaming: false,
53
+ messageStreaming: true,
54
+ reasoningStreaming: false,
55
+ toolCallStreaming: true,
56
+ hiddenReasoningReplay: "none",
57
+ replayFidelity: "high",
58
+ tools: true,
59
+ usage: "none",
60
+ billing: "none",
61
+ providerMetadata: true
50
62
  }
51
63
  };
52
64
  //#endregion
@@ -491,17 +503,14 @@ function createEventFactory(state) {
491
503
  }
492
504
  //#endregion
493
505
  //#region src/core/aggregator.ts
494
- function createInitialState() {
506
+ function createAggregatorState() {
495
507
  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
508
  auxiliary: {},
504
- warnings: []
509
+ warnings: [],
510
+ warningSet: /* @__PURE__ */ new Set(),
511
+ output: [],
512
+ textParts: [],
513
+ toolCalls: []
505
514
  };
506
515
  }
507
516
  function handleResponseStarted(state, event) {
@@ -523,55 +532,16 @@ function handleResponseAuxiliary(state, event) {
523
532
  };
524
533
  if (event.auxiliary) state.auxiliary = mergeAuxiliary$1(state.auxiliary, event.auxiliary);
525
534
  }
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
535
  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);
536
+ state.output.push(event.item);
537
+ pushMessageText(state, event.item);
552
538
  }
553
539
  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);
540
+ state.output.push(event.item);
569
541
  }
570
542
  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);
543
+ state.output.push(event.item);
544
+ state.toolCalls.push(event.item);
575
545
  }
576
546
  function handleResponseCompleted(state, event) {
577
547
  state.replayFromAdapter = event.response.replay;
@@ -590,26 +560,6 @@ function handleResponseCompleted(state, event) {
590
560
  if (event.response.warnings) pushWarnings(state, event.response.warnings);
591
561
  }
592
562
  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
563
  const backendFromResponse = state.backendFromAdapter;
614
564
  const backend = {
615
565
  adapter: backendFromResponse?.adapter ?? state.backendInfo?.kind ?? "unknown",
@@ -621,10 +571,10 @@ function buildResponse(state) {
621
571
  };
622
572
  return {
623
573
  id: state.responseIdFromAdapter ?? state.responseId,
624
- output,
574
+ output: state.output,
625
575
  replay: state.replayFromAdapter ?? [],
626
- text,
627
- toolCalls,
576
+ text: state.textParts.join(""),
577
+ toolCalls: state.toolCalls,
628
578
  stopReason: state.stopReasonFromAdapter,
629
579
  usage: state.usage,
630
580
  billing: state.billing,
@@ -638,8 +588,13 @@ function buildResponse(state) {
638
588
  * 适用于测试和离线处理场景。
639
589
  */
640
590
  function aggregateEvents(events) {
641
- const state = createInitialState();
642
- for (const event of events) switch (event.type) {
591
+ const state = createAggregatorState();
592
+ for (const event of events) aggregateEvent(state, event);
593
+ return finalizeAggregation(state);
594
+ }
595
+ function aggregateEvent(state, event) {
596
+ state.lastEventType = event.type;
597
+ switch (event.type) {
643
598
  case "response.started":
644
599
  handleResponseStarted(state, event);
645
600
  break;
@@ -650,29 +605,17 @@ function aggregateEvents(events) {
650
605
  handleResponseAuxiliary(state, event);
651
606
  break;
652
607
  case "message.started":
653
- handleMessageStarted(state, event);
654
- break;
655
608
  case "message.delta":
656
- handleMessageDelta(state, event);
657
- break;
658
- case "message.completed":
659
- handleMessageCompleted(state, event);
660
- break;
661
609
  case "reasoning.started":
662
- handleReasoningStarted(state, event);
663
- break;
664
610
  case "reasoning.delta":
665
- handleReasoningDelta(state, event);
611
+ case "tool_call.started":
612
+ case "tool_call.delta": break;
613
+ case "message.completed":
614
+ handleMessageCompleted(state, event);
666
615
  break;
667
616
  case "reasoning.completed":
668
617
  handleReasoningCompleted(state, event);
669
618
  break;
670
- case "tool_call.started":
671
- handleToolCallStarted(state, event);
672
- break;
673
- case "tool_call.delta":
674
- handleToolCallDelta(state, event);
675
- break;
676
619
  case "tool_call.completed":
677
620
  handleToolCallCompleted(state, event);
678
621
  break;
@@ -680,8 +623,9 @@ function aggregateEvents(events) {
680
623
  handleResponseCompleted(state, event);
681
624
  break;
682
625
  }
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");
626
+ }
627
+ function finalizeAggregation(state) {
628
+ if (state.lastEventType !== "response.completed") throw new Error("Stream must end with response.completed event to produce a valid AIResponse");
685
629
  return buildResponse(state);
686
630
  }
687
631
  function mergeAuxiliary$1(base, patch) {
@@ -690,20 +634,26 @@ function mergeAuxiliary$1(base, patch) {
690
634
  ...patch
691
635
  };
692
636
  if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
693
- ...base.providerMetadata ?? {},
694
- ...patch.providerMetadata ?? {}
637
+ ...base.providerMetadata,
638
+ ...patch.providerMetadata
695
639
  };
696
640
  return merged;
697
641
  }
698
642
  function pushWarnings(state, warnings) {
699
- for (const warning of warnings) if (!state.warnings.includes(warning)) state.warnings.push(warning);
643
+ for (const warning of warnings) if (!state.warningSet.has(warning)) {
644
+ state.warningSet.add(warning);
645
+ state.warnings.push(warning);
646
+ }
647
+ }
648
+ function pushMessageText(state, item) {
649
+ for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
700
650
  }
701
651
  //#endregion
702
652
  //#region src/core/collect-stream.ts
703
653
  async function collectStream(stream) {
704
- const events = [];
705
- for await (const event of stream) events.push(event);
706
- return aggregateEvents(events);
654
+ const state = createAggregatorState();
655
+ for await (const event of stream) aggregateEvent(state, event);
656
+ return finalizeAggregation(state);
707
657
  }
708
658
  //#endregion
709
659
  //#region src/helpers/mapping.ts
@@ -1102,12 +1052,12 @@ var AdapterBase = class {
1102
1052
  function mergeAuxiliary(base, patch) {
1103
1053
  if (!base && !patch) return void 0;
1104
1054
  const merged = {
1105
- ...base ?? {},
1106
- ...patch ?? {}
1055
+ ...base,
1056
+ ...patch
1107
1057
  };
1108
1058
  if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
1109
- ...base?.providerMetadata ?? {},
1110
- ...patch?.providerMetadata ?? {}
1059
+ ...base?.providerMetadata,
1060
+ ...patch?.providerMetadata
1111
1061
  };
1112
1062
  return merged;
1113
1063
  }
@@ -1189,6 +1139,7 @@ function parseSSEEvents(chunk) {
1189
1139
  function ensureResponsesTextBlocks(blocks, field) {
1190
1140
  for (let i = 0; i < blocks.length; i++) {
1191
1141
  const block = blocks[i];
1142
+ if (!block) continue;
1192
1143
  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
1144
  }
1194
1145
  return blocks;
@@ -1468,6 +1419,7 @@ var ResponsesAdapter = class extends AdapterBase {
1468
1419
  function ensureMessagesTextBlocks(blocks, field) {
1469
1420
  for (let i = 0; i < blocks.length; i++) {
1470
1421
  const block = blocks[i];
1422
+ if (!block) continue;
1471
1423
  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
1424
  }
1473
1425
  return blocks;
@@ -1932,6 +1884,7 @@ function parseChatSSE(buffer) {
1932
1884
  function ensureTextCompatibleBlocks(blocks, field) {
1933
1885
  for (let i = 0; i < blocks.length; i++) {
1934
1886
  const block = blocks[i];
1887
+ if (!block) continue;
1935
1888
  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
1889
  }
1937
1890
  return blocks;
@@ -2343,6 +2296,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2343
2296
  function ensureOllamaTextBlocks(blocks, field) {
2344
2297
  for (let i = 0; i < blocks.length; i++) {
2345
2298
  const block = blocks[i];
2299
+ if (!block) continue;
2346
2300
  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
2301
  }
2348
2302
  return blocks;
@@ -2651,6 +2605,327 @@ var OllamaAdapter = class extends AdapterBase {
2651
2605
  }
2652
2606
  };
2653
2607
  //#endregion
2608
+ //#region src/adapters/mock.ts
2609
+ /**
2610
+ * Mock Adapter
2611
+ *
2612
+ * 面向测试的脚本化 adapter:
2613
+ * - 按 turn 顺序消费请求,验证调用方是否正确续接 replay / tool_result
2614
+ * - 发出可控的 message / reasoning / tool_call 流
2615
+ * - 注入 warning / auxiliary / content_filter / 中断 / provider error
2616
+ *
2617
+ * 这不是通用“假模型”,而是测试工具调用编排与错误路径的测试夹具。
2618
+ */
2619
+ var MockAdapter = class extends AdapterBase {
2620
+ kind = "mock";
2621
+ capabilities = CAPABILITY_MATRIX.mock;
2622
+ turns;
2623
+ onExhausted;
2624
+ providerMetadata;
2625
+ cursor = 0;
2626
+ previousReplay = [];
2627
+ pendingToolCalls = [];
2628
+ history = [];
2629
+ activeStream = false;
2630
+ constructor(options) {
2631
+ super();
2632
+ this.turns = options.turns;
2633
+ this.onExhausted = options.onExhausted ?? "throw";
2634
+ this.providerMetadata = options.providerMetadata;
2635
+ }
2636
+ async buildRequest(request) {
2637
+ const turnIndex = this.cursor;
2638
+ const turn = this.resolveTurn(turnIndex);
2639
+ const turnName = turn.name;
2640
+ const context = this.buildTurnContext(turnIndex);
2641
+ if (turn.expect) if (typeof turn.expect === "function") await turn.expect(request, context);
2642
+ else assertRequestMatchesExpectation(request, turn.expect, context);
2643
+ const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
2644
+ this.cursor += 1;
2645
+ return {
2646
+ request,
2647
+ turn,
2648
+ turnIndex,
2649
+ turnName,
2650
+ remainingPendingToolCalls
2651
+ };
2652
+ }
2653
+ async *runStream(providerRequest, factory, request) {
2654
+ if (this.activeStream) throw new AIRequestError("MockAdapter does not support concurrent streams", "MOCK_CONCURRENT_STREAM");
2655
+ this.activeStream = true;
2656
+ try {
2657
+ const mockRequest = providerRequest;
2658
+ const output = [];
2659
+ for (const [stepIndex, step] of mockRequest.turn.steps.entries()) switch (step.type) {
2660
+ case "warning":
2661
+ yield factory.responseWarning(step.message, step.code);
2662
+ break;
2663
+ case "auxiliary":
2664
+ yield factory.responseAuxiliary({
2665
+ usage: step.usage,
2666
+ billing: step.billing,
2667
+ auxiliary: step.auxiliary
2668
+ });
2669
+ break;
2670
+ case "message": {
2671
+ const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepIndex);
2672
+ yield* emitMessage(factory, item);
2673
+ output.push(item);
2674
+ break;
2675
+ }
2676
+ case "reasoning": {
2677
+ const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepIndex);
2678
+ yield* emitReasoning(factory, item);
2679
+ output.push(item);
2680
+ break;
2681
+ }
2682
+ case "tool_call": {
2683
+ const item = createToolCallFromStep(step);
2684
+ yield* emitToolCall(factory, item, step.streamArguments ?? true);
2685
+ output.push(item);
2686
+ break;
2687
+ }
2688
+ case "output": {
2689
+ assertSupportedOutputItem(step.item);
2690
+ const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepIndex);
2691
+ yield* emitOutputItem(factory, item);
2692
+ output.push(item);
2693
+ break;
2694
+ }
2695
+ case "complete": {
2696
+ const response = this.finalizeTurn(request, factory, mockRequest, output, step);
2697
+ yield factory.responseCompleted(response);
2698
+ return;
2699
+ }
2700
+ case "error": {
2701
+ yield factory.responseWarning(step.message, step.code);
2702
+ const response = this.finalizeTurn(request, factory, mockRequest, output, {
2703
+ type: "complete",
2704
+ stopReason: step.stopReason ?? "error",
2705
+ providerMetadata: step.providerMetadata
2706
+ });
2707
+ yield factory.responseCompleted(response);
2708
+ return;
2709
+ }
2710
+ case "interrupt":
2711
+ this.pendingToolCalls = mockRequest.remainingPendingToolCalls;
2712
+ return;
2713
+ case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2714
+ }
2715
+ const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" });
2716
+ yield factory.responseCompleted(response);
2717
+ } finally {
2718
+ this.activeStream = false;
2719
+ }
2720
+ }
2721
+ finalizeTurn(request, factory, mockRequest, output, completion) {
2722
+ const replay = completion.replay ?? replayFromOutput(output);
2723
+ const toolCalls = output.filter((item) => item.type === "tool_call");
2724
+ this.previousReplay = replay;
2725
+ this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
2726
+ this.history.push({
2727
+ turnIndex: mockRequest.turnIndex,
2728
+ turnName: mockRequest.turnName,
2729
+ requestId: request.requestId,
2730
+ replay,
2731
+ toolCalls
2732
+ });
2733
+ return this.buildResponse(request, {
2734
+ output,
2735
+ replay,
2736
+ stopReason: completion.stopReason ?? resolveStopReason(output),
2737
+ usage: completion.usage,
2738
+ billing: completion.billing,
2739
+ auxiliary: completion.auxiliary,
2740
+ providerMetadata: {
2741
+ turnIndex: mockRequest.turnIndex,
2742
+ turnName: mockRequest.turnName,
2743
+ scriptedSteps: mockRequest.turn.steps.length,
2744
+ pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
2745
+ historyLength: this.history.length,
2746
+ ...this.providerMetadata,
2747
+ ...completion.providerMetadata
2748
+ },
2749
+ warnings: completion.warnings,
2750
+ metadataSources: ["mock"],
2751
+ rawResponseId: completion.rawResponseId
2752
+ }, factory);
2753
+ }
2754
+ resolveTurn(turnIndex) {
2755
+ const turn = this.turns[turnIndex];
2756
+ if (turn !== void 0) return turn;
2757
+ const lastTurn = this.turns.at(-1);
2758
+ if (this.onExhausted === "repeat-last" && lastTurn !== void 0) return lastTurn;
2759
+ if (this.onExhausted === "complete-empty") return {
2760
+ name: "exhausted",
2761
+ steps: []
2762
+ };
2763
+ throw new AIRequestError(`MockAdapter turn ${turnIndex + 1} requested, but only ${this.turns.length} turn(s) were scripted`, "MOCK_TURN_EXHAUSTED");
2764
+ }
2765
+ buildTurnContext(turnIndex) {
2766
+ return {
2767
+ turnIndex,
2768
+ previousReplay: this.previousReplay.map(cloneItem),
2769
+ pendingToolCalls: this.pendingToolCalls.map(cloneItem),
2770
+ history: this.history.map((record) => ({
2771
+ ...record,
2772
+ replay: record.replay.map(cloneItem),
2773
+ toolCalls: record.toolCalls.map(cloneItem)
2774
+ }))
2775
+ };
2776
+ }
2777
+ };
2778
+ function createMessageFromStep(step, request, turnIndex, stepIndex) {
2779
+ return {
2780
+ ...messageItem(normalizeBlocks(step.content), { id: step.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}` }),
2781
+ role: "assistant"
2782
+ };
2783
+ }
2784
+ function createReasoningFromStep(step, request, turnIndex, stepIndex) {
2785
+ return reasoningItem(normalizeBlocks(step.content), step.visibility ?? "full", step.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`);
2786
+ }
2787
+ function createToolCallFromStep(step) {
2788
+ return {
2789
+ type: "tool_call",
2790
+ id: step.id,
2791
+ name: step.name,
2792
+ argumentsText: step.argumentsText,
2793
+ argumentsJson: step.argumentsJson
2794
+ };
2795
+ }
2796
+ function normalizeBlocks(content) {
2797
+ return typeof content === "string" ? [textBlock(content)] : content;
2798
+ }
2799
+ function assertSupportedOutputItem(item) {
2800
+ if (item.type === "opaque") throw new AIRequestError("MockAdapter does not stream opaque output items; use complete.replay if needed", "MOCK_OPAQUE_OUTPUT");
2801
+ }
2802
+ function attachSyntheticId(item, request, turnIndex, stepIndex) {
2803
+ if (item.type === "message") return {
2804
+ ...item,
2805
+ id: item.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}`,
2806
+ role: "assistant"
2807
+ };
2808
+ if (item.type === "reasoning") return {
2809
+ ...item,
2810
+ id: item.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`
2811
+ };
2812
+ return item;
2813
+ }
2814
+ async function* emitOutputItem(factory, item) {
2815
+ if (item.type === "message") {
2816
+ yield* emitMessage(factory, item);
2817
+ return;
2818
+ }
2819
+ if (item.type === "reasoning") {
2820
+ yield* emitReasoning(factory, item);
2821
+ return;
2822
+ }
2823
+ yield* emitToolCall(factory, item, true);
2824
+ }
2825
+ async function* emitMessage(factory, item) {
2826
+ if (!item.id) throw new AIRequestError("Mock message output requires an id after normalization", "MOCK_MESSAGE_ID_MISSING");
2827
+ yield factory.messageStarted(item.id);
2828
+ for (const block of item.content) if (block.type === "text") yield factory.messageDelta(item.id, block.text);
2829
+ yield factory.messageCompleted(item);
2830
+ }
2831
+ async function* emitReasoning(factory, item) {
2832
+ if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
2833
+ yield factory.reasoningStarted(item.id, item.visibility);
2834
+ for (const block of item.content) yield factory.reasoningDelta(item.id, block);
2835
+ yield factory.reasoningCompleted(item);
2836
+ }
2837
+ async function* emitToolCall(factory, item, streamArguments) {
2838
+ yield factory.toolCallStarted(item.id, item.name);
2839
+ if (streamArguments && item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
2840
+ yield factory.toolCallCompleted(item);
2841
+ }
2842
+ function resolveStopReason(output) {
2843
+ return output.some((item) => item.type === "tool_call") ? "tool_call" : "end_turn";
2844
+ }
2845
+ function consumePendingToolCalls(pending, input) {
2846
+ const fulfilledIds = new Set(input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2847
+ return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
2848
+ }
2849
+ function assertRequestMatchesExpectation(request, expectation, context) {
2850
+ const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
2851
+ 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");
2852
+ 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");
2853
+ if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
2854
+ if (expectation.tools === "absent" && request.tools && request.tools.length > 0) throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
2855
+ if (expectation.toolChoice === "present" && request.toolChoice === void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
2856
+ if (expectation.toolChoice === "absent" && request.toolChoice !== void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
2857
+ if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) assertReplayIncluded(request.input, context.previousReplay, prefix);
2858
+ if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
2859
+ const toolResultIds = new Set(request.input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2860
+ 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");
2861
+ }
2862
+ if (expectation.items && expectation.items.length > 0) if (expectation.ordered) assertOrderedItems(request.input, expectation.items, prefix);
2863
+ else assertUnorderedItems(request.input, expectation.items, prefix);
2864
+ }
2865
+ function assertReplayIncluded(input, replay, prefix) {
2866
+ const fingerprints = input.map(fingerprintItem);
2867
+ let cursor = 0;
2868
+ for (const replayItem of replay) {
2869
+ const target = fingerprintItem(replayItem);
2870
+ const foundIndex = fingerprints.indexOf(target, cursor);
2871
+ if (foundIndex === -1) throw new AIRequestError(`${prefix}: previous replay item was not carried into the next request`, "MOCK_EXPECTATION_FAILED");
2872
+ cursor = foundIndex + 1;
2873
+ }
2874
+ }
2875
+ function assertOrderedItems(input, expectations, prefix) {
2876
+ let cursor = 0;
2877
+ for (const expected of expectations) {
2878
+ let matched = false;
2879
+ while (cursor < input.length) {
2880
+ const item = input[cursor];
2881
+ if (item !== void 0 && matchesItemExpectation(item, expected)) {
2882
+ matched = true;
2883
+ cursor += 1;
2884
+ break;
2885
+ }
2886
+ cursor += 1;
2887
+ }
2888
+ if (!matched) throw new AIRequestError(`${prefix}: missing ordered input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
2889
+ }
2890
+ }
2891
+ function assertUnorderedItems(input, expectations, prefix) {
2892
+ for (const expected of expectations) if (!input.some((item) => matchesItemExpectation(item, expected))) throw new AIRequestError(`${prefix}: missing input item ${describeExpectation(expected)}`, "MOCK_EXPECTATION_FAILED");
2893
+ }
2894
+ function matchesItemExpectation(item, expected) {
2895
+ if (item.type !== expected.type) return false;
2896
+ if (expected.id !== void 0 && "id" in item && item.id !== expected.id) return false;
2897
+ switch (item.type) {
2898
+ case "message": return (expected.role === void 0 || item.role === expected.role) && matchesText(item.content, expected.textIncludes);
2899
+ case "reasoning": return (expected.visibility === void 0 || item.visibility === expected.visibility) && matchesText(item.content, expected.textIncludes);
2900
+ case "tool_call": return (expected.name === void 0 || item.name === expected.name) && (expected.textIncludes === void 0 || item.argumentsText.includes(expected.textIncludes));
2901
+ 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);
2902
+ case "opaque": return (expected.source === void 0 || item.source === expected.source) && (expected.purpose === void 0 || item.purpose === expected.purpose);
2903
+ }
2904
+ }
2905
+ function matchesText(blocks, textIncludes) {
2906
+ if (textIncludes === void 0) return true;
2907
+ return blocks.some((block) => {
2908
+ if (block.type === "text") return block.text.includes(textIncludes);
2909
+ if (block.type === "json") return JSON.stringify(block.json).includes(textIncludes);
2910
+ return false;
2911
+ });
2912
+ }
2913
+ function fingerprintItem(item) {
2914
+ return JSON.stringify(item);
2915
+ }
2916
+ function describeExpectation(expectation) {
2917
+ const parts = [`type=${expectation.type}`];
2918
+ if (expectation.role) parts.push(`role=${expectation.role}`);
2919
+ if (expectation.name) parts.push(`name=${expectation.name}`);
2920
+ if (expectation.toolName) parts.push(`toolName=${expectation.toolName}`);
2921
+ if (expectation.callId) parts.push(`callId=${expectation.callId}`);
2922
+ if (expectation.textIncludes) parts.push(`textIncludes=${JSON.stringify(expectation.textIncludes)}`);
2923
+ return `{ ${parts.join(", ")} }`;
2924
+ }
2925
+ function cloneItem(item) {
2926
+ return structuredClone(item);
2927
+ }
2928
+ //#endregion
2654
2929
  //#region src/helpers/synthetic-stream.ts
2655
2930
  /**
2656
2931
  * 模拟流式 (Synthetic Streaming)
@@ -2753,6 +3028,6 @@ function* emitToolCallEvents(item, factory) {
2753
3028
  yield factory.toolCallCompleted(item);
2754
3029
  }
2755
3030
  //#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 };
3031
+ export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, CAPABILITY_MATRIX, 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
3032
 
2758
3033
  //# sourceMappingURL=index.mjs.map