@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 +84 -79
- package/dist/index.d.mts +18 -26
- package/dist/index.mjs +113 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/adapters/chat-completions.ts +5 -4
- package/src/adapters/index.ts +5 -3
- package/src/adapters/messages.ts +20 -10
- package/src/adapters/mock.ts +190 -160
- package/src/adapters/ollama.ts +30 -18
- package/src/adapters/responses.ts +4 -6
- package/src/core/validation.ts +21 -16
- package/src/helpers/adapter-auxiliary.ts +3 -1
- package/src/helpers/adapter-base.ts +5 -5
- package/src/helpers/index.ts +1 -5
- package/src/types/index.ts +1 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nano-ai
|
|
2
2
|
|
|
3
|
-
统一流式 AI 客户端,提供一套 canonical API
|
|
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 {
|
|
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
|
-
//
|
|
142
|
+
// 面向测试的回调驱动 mock backend
|
|
136
143
|
const mock = new MockAdapter({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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`
|
|
173
|
+
`MockAdapter` 是一个面向测试的回调驱动 adapter,用来验证长流程工具调用、`replay` 续接和异常路径。
|
|
169
174
|
|
|
170
|
-
|
|
175
|
+
如果你要调试前端逐字渲染效果,可以用 `withMockStreaming()` 给非流式 handler 包一层分片输出:
|
|
171
176
|
|
|
172
177
|
```ts
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
-
|
|
180
|
-
{
|
|
181
|
-
steps: [{ type: "message", content: "Streaming preview for the frontend." }],
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
});
|
|
187
|
+
);
|
|
185
188
|
```
|
|
186
189
|
|
|
187
|
-
默认会发出单个完整 `message.delta
|
|
190
|
+
默认会发出单个完整 `message.delta`。只有经 `withMockStreaming()` 注入默认流速后,`message` / `reasoning` / `tool_call` 参数才会被拆成多个 delta。单个 step 也可用 `stream: false` 关闭包装器的默认流速配置。
|
|
188
191
|
|
|
189
|
-
|
|
192
|
+
核心思路是每轮请求执行一次 handler:
|
|
190
193
|
|
|
191
|
-
-
|
|
192
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
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: "
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
argumentsText: '{"city":"Hangzhou"}',
|
|
210
|
+
items: [{ type: "message", role: "user", textIncludes: "weather" }],
|
|
211
|
+
tools: "present",
|
|
212
|
+
toolChoice: "present",
|
|
216
213
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
2515
|
-
* -
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|