@codehz/ai 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nano-ai
2
2
 
3
- 统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的脚本化 `MockAdapter`(`responses` / `messages` / `chat.completions` / `ollama` / `mock`)。
3
+ 统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat.completions` / `ollama` / `mock`)。
4
4
 
5
5
  ## 安装
6
6
 
@@ -109,16 +109,23 @@ console.log(response.replay); // 续接材料
109
109
 
110
110
  ## 后端 Adapter
111
111
 
112
- | Adapter | 类 | 说明 |
113
- | ----------------------- | ------------------------ | ------------------------ |
114
- | OpenAI Responses API | `ResponsesAdapter` | OpenAI Responses 端点 |
115
- | Anthropic Messages API | `MessagesAdapter` | Anthropic Messages 端点 |
116
- | OpenAI Chat Completions | `ChatCompletionsAdapter` | Chat Completions 端点 |
117
- | Ollama Chat API | `OllamaAdapter` | 本地或自托管 Ollama |
118
- | Scripted Test Backend | `MockAdapter` | 脚本化测试夹具 |
112
+ | Adapter | 类 | 说明 |
113
+ | ----------------------- | ------------------------ | ----------------------- |
114
+ | OpenAI Responses API | `ResponsesAdapter` | OpenAI Responses 端点 |
115
+ | Anthropic Messages API | `MessagesAdapter` | Anthropic Messages 端点 |
116
+ | OpenAI Chat Completions | `ChatCompletionsAdapter` | Chat Completions 端点 |
117
+ | Ollama Chat API | `OllamaAdapter` | 本地或自托管 Ollama |
118
+ | Scripted Test Backend | `MockAdapter` | 脚本化测试夹具 |
119
119
 
120
120
  ```ts
121
- import { ResponsesAdapter, MessagesAdapter, ChatCompletionsAdapter, OllamaAdapter, MockAdapter } from "nano-ai";
121
+ import {
122
+ ResponsesAdapter,
123
+ MessagesAdapter,
124
+ ChatCompletionsAdapter,
125
+ OllamaAdapter,
126
+ MockAdapter,
127
+ withMockStreaming,
128
+ } from "nano-ai";
122
129
 
123
130
  // OpenAI Responses API
124
131
  const responses = new ResponsesAdapter({ apiKey: "sk-..." });
@@ -132,25 +139,23 @@ const chat = new ChatCompletionsAdapter({ apiKey: "sk-..." });
132
139
  // Ollama
133
140
  const ollama = new OllamaAdapter({ baseUrl: "http://localhost:11434" });
134
141
 
135
- // 面向测试的脚本化 mock backend
142
+ // 面向测试的回调驱动 mock backend
136
143
  const mock = new MockAdapter({
137
- stream: {
138
- charsPerSecond: 24,
139
- chunkSize: 1,
140
- },
141
- turns: [
144
+ handler: withMockStreaming(
145
+ async function* () {
146
+ yield { type: "message", content: "我先调用天气工具。" };
147
+ yield {
148
+ type: "tool_call",
149
+ id: "mock-call-weather",
150
+ name: "get_weather",
151
+ argumentsText: '{"city":"Hangzhou"}',
152
+ };
153
+ },
142
154
  {
143
- steps: [
144
- { type: "message", content: "我先调用天气工具。" },
145
- {
146
- type: "tool_call",
147
- id: "mock-call-weather",
148
- name: "get_weather",
149
- argumentsText: '{"city":"Hangzhou"}',
150
- },
151
- ],
155
+ charsPerSecond: 24,
156
+ chunkSize: 1,
152
157
  },
153
- ],
158
+ ),
154
159
  });
155
160
  ```
156
161
 
@@ -165,66 +170,71 @@ adapter.nativeStreaming; // 是否为 provider 原生流,而不是本地模拟
165
170
 
166
171
  ## Mock 后端
167
172
 
168
- `MockAdapter` 是一个面向测试的脚本化 adapter,用来验证长流程工具调用、`replay` 续接和异常路径。
173
+ `MockAdapter` 是一个面向测试的回调驱动 adapter,用来验证长流程工具调用、`replay` 续接和异常路径。
169
174
 
170
- 如果你要调试前端逐字渲染效果,可以给 `MockAdapter` 打开分片流:
175
+ 如果你要调试前端逐字渲染效果,可以用 `withMockStreaming()` 给非流式 handler 包一层分片输出:
171
176
 
172
177
  ```ts
173
- const mock = new MockAdapter({
174
- stream: {
178
+ const handler = withMockStreaming(
179
+ async function* () {
180
+ yield { type: "message", content: "Streaming preview for the frontend." };
181
+ },
182
+ {
175
183
  charsPerSecond: 20, // 每秒约 20 个字符
176
184
  chunkSize: 1, // 默认 1,即逐字输出
177
185
  initialDelayMs: 150, // 可选:首字前停顿
178
186
  },
179
- turns: [
180
- {
181
- steps: [{ type: "message", content: "Streaming preview for the frontend." }],
182
- },
183
- ],
184
- });
187
+ );
185
188
  ```
186
189
 
187
- 默认会发出单个完整 `message.delta`。只有显式配置 `stream` 时,`message` / `reasoning` / `tool_call` 参数才会被拆成多个 delta。单个 step 也可用 `stream: false` 关闭全局流速配置。
190
+ 默认会发出单个完整 `message.delta`。只有经 `withMockStreaming()` 注入默认流速后,`message` / `reasoning` / `tool_call` 参数才会被拆成多个 delta。单个 step 也可用 `stream: false` 关闭包装器的默认流速配置。
188
191
 
189
- 核心思路是按 turn 写脚本:
192
+ 核心思路是每轮请求执行一次 handler:
190
193
 
191
- - 每一轮可声明对请求格式的期望
192
- - 每一轮可脚本化发出 `message` / `reasoning` / `tool_call`
194
+ - handler 会拿到 `request` 和 `context`
195
+ - `context` 内建 `previousReplay`、`pendingToolCalls`、`history`
196
+ - handler 可脚本化发出 `message` / `reasoning` / `tool_call`
193
197
  - 可注入 `warning`、`content_filter`、transport interruption、provider-style error
194
- - 可验证调用方是否把上一轮 `replay` 和当前 `tool_result` 正确带回
198
+ - 可用 `assertMockRequest()` 验证调用方是否把上一轮 `replay` 和当前 `tool_result` 正确带回
195
199
 
196
200
  ```ts
197
- import { createAIClient, MockAdapter } from "nano-ai";
201
+ import { assertMockRequest, createAIClient, MockAdapter } from "nano-ai";
198
202
 
199
203
  const client = createAIClient({
200
204
  adapter: new MockAdapter({
201
- turns: [
202
- {
203
- name: "request-tool",
204
- expect: {
205
- items: [{ type: "message", role: "user", textIncludes: "weather" }],
206
- tools: "present",
207
- toolChoice: "present",
208
- },
209
- steps: [
210
- { type: "message", content: "Checking weather now." },
205
+ handler: async function* (request, context) {
206
+ if (context.turnIndex === 0) {
207
+ assertMockRequest(
208
+ request,
211
209
  {
212
- type: "tool_call",
213
- id: "mock-call-weather",
214
- name: "get_weather",
215
- argumentsText: '{"city":"Hangzhou"}',
210
+ items: [{ type: "message", role: "user", textIncludes: "weather" }],
211
+ tools: "present",
212
+ toolChoice: "present",
216
213
  },
217
- ],
218
- },
219
- {
220
- name: "consume-tool-result",
221
- expect: {
214
+ context,
215
+ );
216
+
217
+ yield { type: "message", content: "Checking weather now." };
218
+ yield {
219
+ type: "tool_call",
220
+ id: "mock-call-weather",
221
+ name: "get_weather",
222
+ argumentsText: '{"city":"Hangzhou"}',
223
+ };
224
+ return;
225
+ }
226
+
227
+ assertMockRequest(
228
+ request,
229
+ {
222
230
  requireReplayFromPreviousTurn: true,
223
231
  requireToolResultsForPendingCalls: true,
224
232
  },
225
- steps: [{ type: "message", content: "Hangzhou is 28C and sunny." }],
226
- },
227
- ],
233
+ context,
234
+ );
235
+
236
+ yield { type: "message", content: "Hangzhou is 28C and sunny." };
237
+ },
228
238
  }),
229
239
  model: "mock-model",
230
240
  });
@@ -233,10 +243,13 @@ const client = createAIClient({
233
243
  核心类型:
234
244
 
235
245
  ```ts
236
- type MockTurn = {
237
- name?: string;
238
- expect?: MockRequestExpectation | MockTurnValidator;
239
- steps: MockStep[];
246
+ type MockHandler = (request: NormalizedRequest, context: MockHandlerContext) => AsyncIterable<MockStep>;
247
+
248
+ type MockHandlerContext = {
249
+ turnIndex: number;
250
+ previousReplay: ReplayItem[];
251
+ pendingToolCalls: readonly ToolCallItem[];
252
+ history: readonly MockHistoryRecord[];
240
253
  };
241
254
  ```
242
255
 
@@ -248,21 +261,13 @@ type MockTurn = {
248
261
  畸形路径示例:
249
262
 
250
263
  ```ts
251
- {
252
- steps: [
253
- { type: "message", content: "partial answer" },
254
- { type: "interrupt" }, // 不发 response.completed,collectStream() 应失败
255
- ],
256
- }
264
+ yield { type: "message", content: "partial answer" };
265
+ yield { type: "interrupt" }; // 不发 response.completed,collectStream() 应失败
257
266
  ```
258
267
 
259
268
  ```ts
260
- {
261
- steps: [
262
- { type: "warning", message: "content filtered by policy", code: "CONTENT_FILTERED" },
263
- { type: "complete", stopReason: "content_filter" },
264
- ],
265
- }
269
+ yield { type: "warning", message: "content filtered by policy", code: "CONTENT_FILTERED" };
270
+ yield { type: "complete", stopReason: "content_filter" };
266
271
  ```
267
272
 
268
273
  ## 多轮对话
package/dist/index.d.mts CHANGED
@@ -797,13 +797,18 @@ type MockRequestExpectation = {
797
797
  toolChoice?: "ignore" | "present" | "absent";
798
798
  items?: MockInputExpectation[];
799
799
  };
800
- type MockTurnContext = {
800
+ type MockHistoryRecord = {
801
+ turnIndex: number;
802
+ requestId: string;
803
+ replay: ReplayItem[];
804
+ toolCalls: ToolCallItem[];
805
+ };
806
+ type MockHandlerContext = {
801
807
  turnIndex: number;
802
808
  previousReplay: ReplayItem[];
803
809
  pendingToolCalls: readonly ToolCallItem[];
804
- history: readonly MockTurnRecord[];
810
+ history: readonly MockHistoryRecord[];
805
811
  };
806
- type MockTurnValidator = (request: NormalizedRequest, context: MockTurnContext) => void | Promise<void>;
807
812
  type MockWarningStep = {
808
813
  type: "warning";
809
814
  message: string;
@@ -886,38 +891,25 @@ type MockThrowStep = {
886
891
  error: string | Error;
887
892
  };
888
893
  type MockStep = MockWarningStep | MockAuxiliaryStep | MockMessageStep | MockReasoningStep | MockToolCallStep | MockOutputStep | MockCompleteStep | MockErrorStep | MockInterruptStep | MockThrowStep;
889
- type MockTurn = {
890
- name?: string;
891
- expect?: MockRequestExpectation | MockTurnValidator;
892
- steps: MockStep[];
893
- };
894
+ type MockHandler = (request: NormalizedRequest, context: MockHandlerContext) => AsyncIterable<MockStep>;
895
+ type MockHandlerSource = Iterable<MockStep> | AsyncIterable<MockStep>;
896
+ type MockStaticHandler = (request: NormalizedRequest, context: MockHandlerContext) => MockHandlerSource | Promise<MockHandlerSource>;
894
897
  type MockAdapterOptions = {
895
- turns: MockTurn[];
896
- onExhausted?: "throw" | "repeat-last" | "complete-empty";
898
+ handler: MockHandler;
897
899
  providerMetadata?: Record<string, unknown>;
898
- stream?: MockTextStreamOptions;
899
- };
900
- type MockTurnRecord = {
901
- turnIndex: number;
902
- turnName?: string;
903
- requestId: string;
904
- replay: ReplayItem[];
905
- toolCalls: ToolCallItem[];
906
900
  };
907
901
  type MockProviderRequest = {
908
902
  request: NormalizedRequest;
909
- turn: MockTurn;
903
+ handlerResult: AsyncIterable<MockStep>;
910
904
  turnIndex: number;
911
- turnName?: string;
912
905
  remainingPendingToolCalls: ToolCallItem[];
913
906
  };
907
+ declare function assertMockRequest(request: NormalizedRequest, expectation: MockRequestExpectation, context: MockHandlerContext): void;
914
908
  declare class MockAdapter extends AdapterBase {
915
909
  readonly kind: "mock";
916
910
  readonly nativeStreaming = false;
917
- private readonly turns;
918
- private readonly onExhausted;
911
+ private readonly handler;
919
912
  private readonly providerMetadata?;
920
- private readonly defaultStream?;
921
913
  private cursor;
922
914
  private previousReplay;
923
915
  private pendingToolCalls;
@@ -927,9 +919,9 @@ declare class MockAdapter extends AdapterBase {
927
919
  protected buildRequest(request: NormalizedRequest): Promise<MockProviderRequest>;
928
920
  protected runStream(providerRequest: unknown, factory: EventFactory, request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
929
921
  private finalizeTurn;
930
- private resolveTurn;
931
- private buildTurnContext;
922
+ private buildHandlerContext;
932
923
  }
924
+ declare function withMockStreaming(handler: MockStaticHandler, options: MockTextStreamOptions): MockHandler;
933
925
  //#endregion
934
926
  //#region src/helpers/mapping.d.ts
935
927
  declare function mapStopReason(providerReason: string): StopReason;
@@ -1051,5 +1043,5 @@ type SyntheticStreamOptions = {
1051
1043
  */
1052
1044
  declare function syntheticStream(options: SyntheticStreamOptions): AsyncIterable<AIStreamEvent>;
1053
1045
  //#endregion
1054
- export { type AIClient, AIError, AIMappingError, AIProviderError, type AIRequest, AIRequestError, type AIResponse, AIStreamError, type AIStreamEvent, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, type AuxiliaryFinalizeOptions, type AuxiliaryFinalizeResult, type AuxiliaryInfo, type BackendAdapter, type BackendTrace, type BillingInfo, type BillingPostprocessHook, type BillingSource, ChatCompletionsAdapter, type ChatCompletionsAdapterOptions, type ContentBlock, type CreateAIClientOptions, type EventFactory, type EventFactoryBackend, type EventFactoryState, type FetchFn, type IncludeSettings, type InputItem, type LookupResult, type MessageCompletedEvent, type MessageDeltaEvent, type MessageItem, type MessageStartedEvent, MessagesAdapter, type MessagesAdapterOptions, MockAdapter, type MockAdapterOptions, type MockAuxiliaryStep, type MockCompleteStep, type MockErrorStep, type MockInputExpectation, type MockInterruptStep, type MockMessageStep, type MockOutputStep, type MockReasoningStep, type MockRequestExpectation, type MockStep, type MockTextStreamOptions, type MockThrowStep, type MockToolCallStep, type MockTurn, type MockTurnContext, type MockTurnValidator, type MockWarningStep, type NormalizeOptions, type NormalizedRequest, OllamaAdapter, type OllamaAdapterOptions, type OpaqueItem, type OutputItem, type ReasoningCompletedEvent, type ReasoningDeltaEvent, type ReasoningItem, type ReasoningStartedEvent, type ReplayItem, type ResponseAuxiliaryEvent, type ResponseCompletedEvent, type ResponseStartedEvent, type ResponseWarningEvent, ResponsesAdapter, type ResponsesAdapterOptions, type SSEEvent, type StopReason, type StreamEventBase, type StreamResult, type SyntheticStreamOptions, type ToolCallCompletedEvent, type ToolCallDeltaEvent, type ToolCallItem, type ToolCallStartedEvent, type ToolChoice, type ToolDefinition, type ToolResultItem, type Usage, type UsageSource, type ValidationIssue, 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 };
1046
+ export { type AIClient, AIError, AIMappingError, AIProviderError, type AIRequest, AIRequestError, type AIResponse, AIStreamError, type AIStreamEvent, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, type AuxiliaryFinalizeOptions, type AuxiliaryFinalizeResult, type AuxiliaryInfo, type BackendAdapter, type BackendTrace, type BillingInfo, type BillingPostprocessHook, type BillingSource, ChatCompletionsAdapter, type ChatCompletionsAdapterOptions, type ContentBlock, type CreateAIClientOptions, type EventFactory, type EventFactoryBackend, type EventFactoryState, type FetchFn, type IncludeSettings, type InputItem, type LookupResult, type MessageCompletedEvent, type MessageDeltaEvent, type MessageItem, type MessageStartedEvent, MessagesAdapter, type MessagesAdapterOptions, MockAdapter, type MockAdapterOptions, type MockAuxiliaryStep, type MockCompleteStep, type MockErrorStep, type MockHandler, type MockHandlerContext, type MockHistoryRecord, type MockInputExpectation, type MockInterruptStep, type MockMessageStep, type MockOutputStep, type MockReasoningStep, type MockRequestExpectation, type MockStaticHandler, type MockStep, type MockTextStreamOptions, type MockThrowStep, type MockToolCallStep, type MockWarningStep, type NormalizeOptions, type NormalizedRequest, OllamaAdapter, type OllamaAdapterOptions, type OpaqueItem, type OutputItem, type ReasoningCompletedEvent, type ReasoningDeltaEvent, type ReasoningItem, type ReasoningStartedEvent, type ReplayItem, type ResponseAuxiliaryEvent, type ResponseCompletedEvent, type ResponseStartedEvent, type ResponseWarningEvent, ResponsesAdapter, type ResponsesAdapterOptions, type SSEEvent, type StopReason, type StreamEventBase, type StreamResult, type SyntheticStreamOptions, type ToolCallCompletedEvent, type ToolCallDeltaEvent, type ToolCallItem, type ToolCallStartedEvent, type ToolChoice, type ToolDefinition, type ToolResultItem, type Usage, type UsageSource, type ValidationIssue, 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, validateRequest, withMockStreaming };
1055
1047
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -2511,20 +2511,35 @@ var OllamaAdapter = class extends AdapterBase {
2511
2511
  /**
2512
2512
  * Mock Adapter
2513
2513
  *
2514
- * 面向测试的脚本化 adapter:
2515
- * - turn 顺序消费请求,验证调用方是否正确续接 replay / tool_result
2514
+ * 面向测试的回调驱动 adapter:
2515
+ * - 每次请求执行用户提供的 handler
2516
+ * - 验证调用方是否正确续接 replay / tool_result
2516
2517
  * - 发出可控的 message / reasoning / tool_call 流
2517
2518
  * - 注入 warning / auxiliary / content_filter / 中断 / provider error
2518
2519
  *
2519
2520
  * 这不是通用“假模型”,而是测试工具调用编排与错误路径的测试夹具。
2520
2521
  */
2522
+ function assertMockRequest(request, expectation, context) {
2523
+ const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
2524
+ 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");
2525
+ 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");
2526
+ if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
2527
+ if (expectation.tools === "absent" && request.tools && request.tools.length > 0) throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
2528
+ if (expectation.toolChoice === "present" && request.toolChoice === void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
2529
+ if (expectation.toolChoice === "absent" && request.toolChoice !== void 0) throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
2530
+ if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) assertReplayIncluded(request.input, context.previousReplay, prefix);
2531
+ if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
2532
+ const toolResultIds = new Set(request.input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2533
+ 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");
2534
+ }
2535
+ if (expectation.items && expectation.items.length > 0) if (expectation.ordered) assertOrderedItems(request.input, expectation.items, prefix);
2536
+ else assertUnorderedItems(request.input, expectation.items, prefix);
2537
+ }
2521
2538
  var MockAdapter = class extends AdapterBase {
2522
2539
  kind = "mock";
2523
2540
  nativeStreaming = false;
2524
- turns;
2525
- onExhausted;
2541
+ handler;
2526
2542
  providerMetadata;
2527
- defaultStream;
2528
2543
  cursor = 0;
2529
2544
  previousReplay = [];
2530
2545
  pendingToolCalls = [];
@@ -2532,25 +2547,19 @@ var MockAdapter = class extends AdapterBase {
2532
2547
  activeStream = false;
2533
2548
  constructor(options) {
2534
2549
  super();
2535
- this.turns = options.turns;
2536
- this.onExhausted = options.onExhausted ?? "throw";
2550
+ this.handler = options.handler;
2537
2551
  this.providerMetadata = options.providerMetadata;
2538
- this.defaultStream = resolveMockTextStreamOptions(options.stream, "adapter stream");
2539
2552
  }
2540
2553
  async buildRequest(request) {
2541
2554
  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);
2555
+ const context = this.buildHandlerContext(turnIndex);
2547
2556
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
2557
+ const handlerResult = this.handler(request, context);
2548
2558
  this.cursor += 1;
2549
2559
  return {
2550
2560
  request,
2551
- turn,
2561
+ handlerResult,
2552
2562
  turnIndex,
2553
- turnName,
2554
2563
  remainingPendingToolCalls
2555
2564
  };
2556
2565
  }
@@ -2560,76 +2569,79 @@ var MockAdapter = class extends AdapterBase {
2560
2569
  try {
2561
2570
  const mockRequest = providerRequest;
2562
2571
  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;
2572
+ let stepCount = 0;
2573
+ for await (const step of mockRequest.handlerResult) {
2574
+ stepCount += 1;
2575
+ switch (step.type) {
2576
+ case "warning":
2577
+ yield factory.responseWarning(step.message, step.code);
2578
+ break;
2579
+ case "auxiliary":
2580
+ yield factory.responseAuxiliary({
2581
+ usage: step.usage,
2582
+ billing: step.billing,
2583
+ auxiliary: step.auxiliary
2584
+ });
2585
+ break;
2586
+ case "message": {
2587
+ const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
2588
+ yield* emitMessage(factory, item, resolveStepStreamOptions(void 0, step.stream, "message"));
2589
+ output.push(item);
2590
+ break;
2591
+ }
2592
+ case "reasoning": {
2593
+ const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
2594
+ yield* emitReasoning(factory, item, resolveStepStreamOptions(void 0, step.stream, "reasoning"));
2595
+ output.push(item);
2596
+ break;
2597
+ }
2598
+ case "tool_call": {
2599
+ const item = createToolCallFromStep(step);
2600
+ yield* emitToolCall(factory, item, step.streamArguments ?? true, resolveStepStreamOptions(void 0, step.stream, "tool_call"));
2601
+ output.push(item);
2602
+ break;
2603
+ }
2604
+ case "output": {
2605
+ assertSupportedOutputItem(step.item);
2606
+ const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepCount - 1);
2607
+ yield* emitOutputItem(factory, item, resolveStepStreamOptions(void 0, step.stream, "output"));
2608
+ output.push(item);
2609
+ break;
2610
+ }
2611
+ case "complete": {
2612
+ const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
2613
+ yield factory.responseCompleted(response);
2614
+ return;
2615
+ }
2616
+ case "error": {
2617
+ yield factory.responseWarning(step.message, step.code);
2618
+ const response = this.finalizeTurn(request, factory, mockRequest, output, {
2619
+ type: "complete",
2620
+ stopReason: step.stopReason ?? "error",
2621
+ providerMetadata: step.providerMetadata
2622
+ }, stepCount);
2623
+ yield factory.responseCompleted(response);
2624
+ return;
2625
+ }
2626
+ case "interrupt":
2627
+ this.pendingToolCalls = mockRequest.remainingPendingToolCalls;
2628
+ return;
2629
+ case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
2613
2630
  }
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
2631
  }
2619
- const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" });
2632
+ const response = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
2620
2633
  yield factory.responseCompleted(response);
2621
2634
  } finally {
2622
2635
  this.activeStream = false;
2623
2636
  }
2624
2637
  }
2625
- finalizeTurn(request, factory, mockRequest, output, completion) {
2638
+ finalizeTurn(request, factory, mockRequest, output, completion, stepCount) {
2626
2639
  const replay = completion.replay ?? replayFromOutput(output);
2627
2640
  const toolCalls = output.filter((item) => item.type === "tool_call");
2628
2641
  this.previousReplay = replay;
2629
2642
  this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
2630
2643
  this.history.push({
2631
2644
  turnIndex: mockRequest.turnIndex,
2632
- turnName: mockRequest.turnName,
2633
2645
  requestId: request.requestId,
2634
2646
  replay,
2635
2647
  toolCalls
@@ -2643,8 +2655,7 @@ var MockAdapter = class extends AdapterBase {
2643
2655
  auxiliary: completion.auxiliary,
2644
2656
  providerMetadata: {
2645
2657
  turnIndex: mockRequest.turnIndex,
2646
- turnName: mockRequest.turnName,
2647
- scriptedSteps: mockRequest.turn.steps.length,
2658
+ stepCount,
2648
2659
  pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
2649
2660
  historyLength: this.history.length,
2650
2661
  ...this.providerMetadata,
@@ -2655,18 +2666,7 @@ var MockAdapter = class extends AdapterBase {
2655
2666
  rawResponseId: completion.rawResponseId
2656
2667
  }, factory);
2657
2668
  }
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) {
2669
+ buildHandlerContext(turnIndex) {
2670
2670
  return {
2671
2671
  turnIndex,
2672
2672
  previousReplay: this.previousReplay.map(cloneItem),
@@ -2679,6 +2679,32 @@ var MockAdapter = class extends AdapterBase {
2679
2679
  };
2680
2680
  }
2681
2681
  };
2682
+ function withMockStreaming(handler, options) {
2683
+ const defaults = resolveMockTextStreamOptions(options, "mock stream wrapper");
2684
+ if (!defaults) throw new AIRequestError("mock stream wrapper requires streaming options", "MOCK_STREAM_CONFIG_INVALID");
2685
+ return async function* streamWrappedHandler(request, context) {
2686
+ const source = await handler(request, context);
2687
+ for await (const step of source) yield applyDefaultStreaming(step, defaults);
2688
+ };
2689
+ }
2690
+ function applyDefaultStreaming(step, defaults) {
2691
+ switch (step.type) {
2692
+ case "message":
2693
+ case "reasoning":
2694
+ case "tool_call":
2695
+ case "output":
2696
+ if (step.stream !== void 0) return step;
2697
+ return {
2698
+ ...step,
2699
+ stream: {
2700
+ charsPerSecond: defaults.charsPerSecond,
2701
+ chunkSize: defaults.chunkSize,
2702
+ initialDelayMs: defaults.initialDelayMs
2703
+ }
2704
+ };
2705
+ default: return step;
2706
+ }
2707
+ }
2682
2708
  function createMessageFromStep(step, request, turnIndex, stepIndex) {
2683
2709
  return {
2684
2710
  ...messageItem(normalizeBlocks(step.content), { id: step.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}` }),
@@ -2811,22 +2837,6 @@ function consumePendingToolCalls(pending, input) {
2811
2837
  const fulfilledIds = new Set(input.filter((item) => item.type === "tool_result").map((item) => item.callId));
2812
2838
  return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
2813
2839
  }
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
2840
  function assertReplayIncluded(input, replay, prefix) {
2831
2841
  const fingerprints = input.map(fingerprintItem);
2832
2842
  let cursor = 0;
@@ -2993,6 +3003,6 @@ function* emitToolCallEvents(item, factory) {
2993
3003
  yield factory.toolCallCompleted(item);
2994
3004
  }
2995
3005
  //#endregion
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 };
3006
+ 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, validateRequest, withMockStreaming };
2997
3007
 
2998
3008
  //# sourceMappingURL=index.mjs.map