@codehz/ai 0.4.6 → 0.7.0
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 +221 -75
- package/dist/index.d.mts +662 -523
- package/dist/index.mjs +3626 -2202
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -8
- package/.github/workflows/publish.yml +0 -56
- package/.oxfmtrc.json +0 -12
- package/.oxlintrc.json +0 -34
- package/AGENTS.md +0 -37
- package/src/adapters/chat-completions.ts +0 -624
- package/src/adapters/index.ts +0 -44
- package/src/adapters/messages.ts +0 -635
- package/src/adapters/mock.ts +0 -934
- package/src/adapters/ollama.ts +0 -526
- package/src/adapters/responses.ts +0 -818
- package/src/core/aggregator.ts +0 -428
- package/src/core/client.ts +0 -36
- package/src/core/collect-stream.ts +0 -19
- package/src/core/errors.ts +0 -105
- package/src/core/event-factory.ts +0 -151
- package/src/core/index.ts +0 -18
- package/src/core/merge-auxiliary.ts +0 -22
- package/src/core/normalize.ts +0 -65
- package/src/core/validation.ts +0 -404
- package/src/helpers/adapter-auxiliary.ts +0 -155
- package/src/helpers/adapter-base.ts +0 -218
- package/src/helpers/adapter-security.ts +0 -126
- package/src/helpers/auxiliary-collector.ts +0 -166
- package/src/helpers/incremental-stream-parser.ts +0 -142
- package/src/helpers/index.ts +0 -87
- package/src/helpers/mapping.ts +0 -192
- package/src/helpers/provider-request-options.ts +0 -25
- package/src/helpers/provider-stream.ts +0 -147
- package/src/helpers/reasoning-level.ts +0 -86
- package/src/helpers/request-mapper.ts +0 -94
- package/src/helpers/synthetic-stream.ts +0 -188
- package/src/helpers/usage-mapping.ts +0 -110
- package/src/index.ts +0 -17
- package/src/types/adapter.ts +0 -42
- package/src/types/content.ts +0 -15
- package/src/types/events.ts +0 -138
- package/src/types/index.ts +0 -49
- package/src/types/items.ts +0 -57
- package/src/types/request.ts +0 -52
- package/src/types/response.ts +0 -68
- package/tsdown.config.ts +0 -10
package/src/adapters/mock.ts
DELETED
|
@@ -1,934 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock Adapter
|
|
3
|
-
*
|
|
4
|
-
* 面向测试的回调驱动 adapter:
|
|
5
|
-
* - 每次请求执行用户提供的 handler
|
|
6
|
-
* - 验证调用方是否正确续接 replay / tool_result
|
|
7
|
-
* - 发出可控的 message / reasoning / tool_call 流
|
|
8
|
-
* - 注入 warning / auxiliary / content_filter / 中断 / provider error
|
|
9
|
-
*
|
|
10
|
-
* 这不是通用“假模型”,而是测试工具调用编排与错误路径的测试夹具。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { AIRequestError } from "../core/errors.js";
|
|
14
|
-
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
15
|
-
import { messageItem, reasoningItem, replayFromOutput, textBlock } from "../helpers/mapping.js";
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
AIStreamEvent,
|
|
19
|
-
AuxiliaryInfo,
|
|
20
|
-
BillingInfo,
|
|
21
|
-
ContentBlock,
|
|
22
|
-
EventFactory,
|
|
23
|
-
InputItem,
|
|
24
|
-
MessageItem,
|
|
25
|
-
NormalizedRequest,
|
|
26
|
-
OutputItem,
|
|
27
|
-
ReasoningLevel,
|
|
28
|
-
ReplayItem,
|
|
29
|
-
StopReason,
|
|
30
|
-
ToolCallItem,
|
|
31
|
-
ToolResultItem,
|
|
32
|
-
Usage,
|
|
33
|
-
} from "../index.js";
|
|
34
|
-
|
|
35
|
-
export type MockInputExpectation = {
|
|
36
|
-
type: InputItem["type"];
|
|
37
|
-
id?: string;
|
|
38
|
-
role?: MessageItem["role"];
|
|
39
|
-
name?: string;
|
|
40
|
-
toolName?: string;
|
|
41
|
-
callId?: string;
|
|
42
|
-
outcome?: ToolResultItem["outcome"];
|
|
43
|
-
visibility?: Extract<InputItem, { type: "reasoning" }>["visibility"];
|
|
44
|
-
source?: Extract<InputItem, { type: "opaque" }>["source"];
|
|
45
|
-
purpose?: Extract<InputItem, { type: "opaque" }>["purpose"];
|
|
46
|
-
textIncludes?: string;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type MockRequestExpectation = {
|
|
50
|
-
minItems?: number;
|
|
51
|
-
maxItems?: number;
|
|
52
|
-
ordered?: boolean;
|
|
53
|
-
requireReplayFromPreviousTurn?: boolean;
|
|
54
|
-
requireToolResultsForPendingCalls?: boolean;
|
|
55
|
-
tools?: "ignore" | "present" | "absent";
|
|
56
|
-
toolChoice?: "ignore" | "present" | "absent";
|
|
57
|
-
items?: MockInputExpectation[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export type MockHistoryRecord = {
|
|
61
|
-
turnIndex: number;
|
|
62
|
-
requestId: string;
|
|
63
|
-
replay: ReplayItem[];
|
|
64
|
-
toolCalls: ToolCallItem[];
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export type MockHandlerContext = {
|
|
68
|
-
turnIndex: number;
|
|
69
|
-
previousReplay: ReplayItem[];
|
|
70
|
-
pendingToolCalls: readonly ToolCallItem[];
|
|
71
|
-
history: readonly MockHistoryRecord[];
|
|
72
|
-
/** 请求的 AbortSignal,handler 可检查 signal.aborted 提前退出。 */
|
|
73
|
-
signal?: AbortSignal;
|
|
74
|
-
/** 当前请求的 portable reasoningLevel(若设置)。 */
|
|
75
|
-
reasoningLevel?: ReasoningLevel;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export type MockWarningStep = {
|
|
79
|
-
type: "warning";
|
|
80
|
-
message: string;
|
|
81
|
-
code?: string;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export type MockAuxiliaryStep = {
|
|
85
|
-
type: "auxiliary";
|
|
86
|
-
usage?: Usage;
|
|
87
|
-
billing?: BillingInfo;
|
|
88
|
-
auxiliary?: Partial<AuxiliaryInfo>;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export type MockTextStreamOptions = {
|
|
92
|
-
/**
|
|
93
|
-
* 每秒吐出的字符数。未设置时仍会按 chunk 拆分,但不会额外等待。
|
|
94
|
-
*/
|
|
95
|
-
charsPerSecond?: number;
|
|
96
|
-
/**
|
|
97
|
-
* 每个 delta 最多包含多少个字符,默认 1。
|
|
98
|
-
*/
|
|
99
|
-
chunkSize?: number;
|
|
100
|
-
/**
|
|
101
|
-
* 首个 delta 发出前的延迟。
|
|
102
|
-
*/
|
|
103
|
-
initialDelayMs?: number;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export type MockMessageStep = {
|
|
107
|
-
type: "message";
|
|
108
|
-
id?: string;
|
|
109
|
-
content: string | ContentBlock[];
|
|
110
|
-
stream?: MockTextStreamOptions | false;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export type MockReasoningStep = {
|
|
114
|
-
type: "reasoning";
|
|
115
|
-
id?: string;
|
|
116
|
-
visibility?: Extract<OutputItem, { type: "reasoning" }>["visibility"];
|
|
117
|
-
content: string | ContentBlock[];
|
|
118
|
-
stream?: MockTextStreamOptions | false;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export type MockToolCallStep = {
|
|
122
|
-
type: "tool_call";
|
|
123
|
-
id: string;
|
|
124
|
-
name: string;
|
|
125
|
-
argumentsText: string;
|
|
126
|
-
streamArguments?: boolean;
|
|
127
|
-
stream?: MockTextStreamOptions | false;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export type MockOutputStep = {
|
|
131
|
-
type: "output";
|
|
132
|
-
item: Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }>;
|
|
133
|
-
stream?: MockTextStreamOptions | false;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export type MockCompleteStep = {
|
|
137
|
-
type: "complete";
|
|
138
|
-
stopReason?: StopReason;
|
|
139
|
-
replay?: ReplayItem[];
|
|
140
|
-
usage?: Usage;
|
|
141
|
-
billing?: BillingInfo;
|
|
142
|
-
auxiliary?: Partial<AuxiliaryInfo>;
|
|
143
|
-
providerMetadata?: Record<string, unknown>;
|
|
144
|
-
rawResponseId?: string;
|
|
145
|
-
warnings?: string[];
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export type MockErrorStep = {
|
|
149
|
-
type: "error";
|
|
150
|
-
message: string;
|
|
151
|
-
code?: string;
|
|
152
|
-
stopReason?: StopReason;
|
|
153
|
-
providerMetadata?: Record<string, unknown>;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
export type MockInterruptStep = {
|
|
157
|
-
type: "interrupt";
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export type MockThrowStep = {
|
|
161
|
-
type: "throw";
|
|
162
|
-
error: string | Error;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
export type MockStep =
|
|
166
|
-
| MockWarningStep
|
|
167
|
-
| MockAuxiliaryStep
|
|
168
|
-
| MockMessageStep
|
|
169
|
-
| MockReasoningStep
|
|
170
|
-
| MockToolCallStep
|
|
171
|
-
| MockOutputStep
|
|
172
|
-
| MockCompleteStep
|
|
173
|
-
| MockErrorStep
|
|
174
|
-
| MockInterruptStep
|
|
175
|
-
| MockThrowStep;
|
|
176
|
-
|
|
177
|
-
export type MockHandler = (request: NormalizedRequest, context: MockHandlerContext) => AsyncIterable<MockStep>;
|
|
178
|
-
|
|
179
|
-
type MockHandlerSource = Iterable<MockStep> | AsyncIterable<MockStep>;
|
|
180
|
-
|
|
181
|
-
export type MockStaticHandler = (
|
|
182
|
-
request: NormalizedRequest,
|
|
183
|
-
context: MockHandlerContext,
|
|
184
|
-
) => MockHandlerSource | Promise<MockHandlerSource>;
|
|
185
|
-
|
|
186
|
-
export type MockAdapterOptions = {
|
|
187
|
-
handler: MockHandler;
|
|
188
|
-
providerMetadata?: Record<string, unknown>;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
type MockProviderRequest = {
|
|
192
|
-
request: NormalizedRequest;
|
|
193
|
-
handlerResult: AsyncIterable<MockStep>;
|
|
194
|
-
turnIndex: number;
|
|
195
|
-
remainingPendingToolCalls: ToolCallItem[];
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
type ResolvedMockTextStreamOptions = {
|
|
199
|
-
charsPerSecond?: number;
|
|
200
|
-
chunkSize: number;
|
|
201
|
-
initialDelayMs: number;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
export function assertMockRequest(
|
|
205
|
-
request: NormalizedRequest,
|
|
206
|
-
expectation: MockRequestExpectation,
|
|
207
|
-
context: MockHandlerContext,
|
|
208
|
-
): void {
|
|
209
|
-
const prefix = `MockAdapter turn ${context.turnIndex + 1} expectation failed`;
|
|
210
|
-
|
|
211
|
-
if (expectation.minItems !== undefined && request.input.length < expectation.minItems) {
|
|
212
|
-
throw new AIRequestError(
|
|
213
|
-
`${prefix}: expected at least ${expectation.minItems} input item(s)`,
|
|
214
|
-
"MOCK_EXPECTATION_FAILED",
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (expectation.maxItems !== undefined && request.input.length > expectation.maxItems) {
|
|
219
|
-
throw new AIRequestError(
|
|
220
|
-
`${prefix}: expected at most ${expectation.maxItems} input item(s)`,
|
|
221
|
-
"MOCK_EXPECTATION_FAILED",
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (expectation.tools === "present" && (!request.tools || request.tools.length === 0)) {
|
|
226
|
-
throw new AIRequestError(`${prefix}: expected tools to be present`, "MOCK_EXPECTATION_FAILED");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (expectation.tools === "absent" && request.tools && request.tools.length > 0) {
|
|
230
|
-
throw new AIRequestError(`${prefix}: expected tools to be absent`, "MOCK_EXPECTATION_FAILED");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (expectation.toolChoice === "present" && request.toolChoice === undefined) {
|
|
234
|
-
throw new AIRequestError(`${prefix}: expected toolChoice to be present`, "MOCK_EXPECTATION_FAILED");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (expectation.toolChoice === "absent" && request.toolChoice !== undefined) {
|
|
238
|
-
throw new AIRequestError(`${prefix}: expected toolChoice to be absent`, "MOCK_EXPECTATION_FAILED");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (expectation.requireReplayFromPreviousTurn && context.previousReplay.length > 0) {
|
|
242
|
-
assertReplayIncluded(request.input, context.previousReplay, prefix);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (expectation.requireToolResultsForPendingCalls && context.pendingToolCalls.length > 0) {
|
|
246
|
-
const toolResultIds = new Set(
|
|
247
|
-
request.input.filter((item): item is ToolResultItem => item.type === "tool_result").map((item) => item.callId),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
for (const call of context.pendingToolCalls) {
|
|
251
|
-
if (!toolResultIds.has(call.id)) {
|
|
252
|
-
throw new AIRequestError(
|
|
253
|
-
`${prefix}: expected tool_result for pending tool call "${call.id}"`,
|
|
254
|
-
"MOCK_EXPECTATION_FAILED",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (expectation.items && expectation.items.length > 0) {
|
|
261
|
-
if (expectation.ordered) {
|
|
262
|
-
assertOrderedItems(request.input, expectation.items, prefix);
|
|
263
|
-
} else {
|
|
264
|
-
assertUnorderedItems(request.input, expectation.items, prefix);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export class MockAdapter extends AdapterBase {
|
|
270
|
-
readonly kind = "mock" as const;
|
|
271
|
-
readonly isSyntheticStream = true;
|
|
272
|
-
|
|
273
|
-
private readonly handler: MockHandler;
|
|
274
|
-
private readonly providerMetadata?: Record<string, unknown>;
|
|
275
|
-
|
|
276
|
-
private cursor = 0;
|
|
277
|
-
private previousReplay: ReplayItem[] = [];
|
|
278
|
-
private pendingToolCalls: ToolCallItem[] = [];
|
|
279
|
-
private history: MockHistoryRecord[] = [];
|
|
280
|
-
private activeStream = false;
|
|
281
|
-
|
|
282
|
-
constructor(options: MockAdapterOptions) {
|
|
283
|
-
super();
|
|
284
|
-
this.handler = options.handler;
|
|
285
|
-
this.providerMetadata = options.providerMetadata;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
protected async buildRequest(request: NormalizedRequest): Promise<MockProviderRequest> {
|
|
289
|
-
const turnIndex = this.cursor;
|
|
290
|
-
const context = this.buildHandlerContext(turnIndex, request);
|
|
291
|
-
const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
|
|
292
|
-
const handlerResult = this.handler(request, context);
|
|
293
|
-
|
|
294
|
-
this.cursor += 1;
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
request,
|
|
298
|
-
handlerResult,
|
|
299
|
-
turnIndex,
|
|
300
|
-
remainingPendingToolCalls,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
protected async *runStream(
|
|
305
|
-
providerRequest: unknown,
|
|
306
|
-
factory: EventFactory,
|
|
307
|
-
request: NormalizedRequest,
|
|
308
|
-
): AsyncIterable<AIStreamEvent> {
|
|
309
|
-
if (this.activeStream) {
|
|
310
|
-
throw new AIRequestError("MockAdapter does not support concurrent streams", "MOCK_CONCURRENT_STREAM");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
this.activeStream = true;
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
const mockRequest = providerRequest as MockProviderRequest;
|
|
317
|
-
const output: OutputItem[] = [];
|
|
318
|
-
let stepCount = 0;
|
|
319
|
-
|
|
320
|
-
for await (const step of mockRequest.handlerResult) {
|
|
321
|
-
// 若 signal 已 abort,停止消费 handler 并返回
|
|
322
|
-
if (request.signal?.aborted) return;
|
|
323
|
-
|
|
324
|
-
stepCount += 1;
|
|
325
|
-
|
|
326
|
-
switch (step.type) {
|
|
327
|
-
case "warning":
|
|
328
|
-
yield factory.responseWarning(step.message, step.code);
|
|
329
|
-
break;
|
|
330
|
-
case "auxiliary":
|
|
331
|
-
yield factory.responseAuxiliary({
|
|
332
|
-
usage: step.usage,
|
|
333
|
-
billing: step.billing,
|
|
334
|
-
auxiliary: step.auxiliary,
|
|
335
|
-
});
|
|
336
|
-
break;
|
|
337
|
-
case "message": {
|
|
338
|
-
const item = createMessageFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
|
|
339
|
-
yield* emitMessage(factory, item, resolveStepStreamOptions(undefined, step.stream, "message"));
|
|
340
|
-
output.push(item);
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
case "reasoning": {
|
|
344
|
-
const item = createReasoningFromStep(step, request, mockRequest.turnIndex, stepCount - 1);
|
|
345
|
-
yield* emitReasoning(factory, item, resolveStepStreamOptions(undefined, step.stream, "reasoning"));
|
|
346
|
-
output.push(item);
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
case "tool_call": {
|
|
350
|
-
const item = createToolCallFromStep(step);
|
|
351
|
-
yield* emitToolCall(
|
|
352
|
-
factory,
|
|
353
|
-
item,
|
|
354
|
-
step.streamArguments ?? true,
|
|
355
|
-
resolveStepStreamOptions(undefined, step.stream, "tool_call"),
|
|
356
|
-
);
|
|
357
|
-
output.push(item);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
case "output": {
|
|
361
|
-
assertSupportedOutputItem(step.item);
|
|
362
|
-
const item = attachSyntheticId(step.item, request, mockRequest.turnIndex, stepCount - 1);
|
|
363
|
-
yield* emitOutputItem(factory, item, resolveStepStreamOptions(undefined, step.stream, "output"));
|
|
364
|
-
output.push(item);
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
case "complete": {
|
|
368
|
-
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
|
|
369
|
-
yield factory.responseCompleted({
|
|
370
|
-
replay: finalResponse.replay,
|
|
371
|
-
stopReason: finalResponse.stopReason,
|
|
372
|
-
trace: finalResponse.backend,
|
|
373
|
-
usage: finalResponse.usage,
|
|
374
|
-
billing: finalResponse.billing,
|
|
375
|
-
auxiliary: finalResponse.auxiliary,
|
|
376
|
-
warnings: finalResponse.warnings,
|
|
377
|
-
});
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
case "error": {
|
|
381
|
-
yield factory.responseWarning(step.message, step.code);
|
|
382
|
-
const finalResponse = this.finalizeTurn(
|
|
383
|
-
request,
|
|
384
|
-
factory,
|
|
385
|
-
mockRequest,
|
|
386
|
-
output,
|
|
387
|
-
{
|
|
388
|
-
type: "complete",
|
|
389
|
-
stopReason: step.stopReason ?? "error",
|
|
390
|
-
providerMetadata: step.providerMetadata,
|
|
391
|
-
},
|
|
392
|
-
stepCount,
|
|
393
|
-
);
|
|
394
|
-
yield factory.responseCompleted({
|
|
395
|
-
replay: finalResponse.replay,
|
|
396
|
-
stopReason: finalResponse.stopReason,
|
|
397
|
-
trace: finalResponse.backend,
|
|
398
|
-
usage: finalResponse.usage,
|
|
399
|
-
billing: finalResponse.billing,
|
|
400
|
-
auxiliary: finalResponse.auxiliary,
|
|
401
|
-
warnings: finalResponse.warnings,
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
case "interrupt":
|
|
406
|
-
this.pendingToolCalls = mockRequest.remainingPendingToolCalls;
|
|
407
|
-
return;
|
|
408
|
-
case "throw":
|
|
409
|
-
throw typeof step.error === "string" ? new Error(step.error) : step.error;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const finalResponse = this.finalizeTurn(
|
|
414
|
-
request,
|
|
415
|
-
factory,
|
|
416
|
-
mockRequest,
|
|
417
|
-
output,
|
|
418
|
-
{
|
|
419
|
-
type: "complete",
|
|
420
|
-
},
|
|
421
|
-
stepCount,
|
|
422
|
-
);
|
|
423
|
-
yield factory.responseCompleted({
|
|
424
|
-
replay: finalResponse.replay,
|
|
425
|
-
stopReason: finalResponse.stopReason,
|
|
426
|
-
trace: finalResponse.backend,
|
|
427
|
-
usage: finalResponse.usage,
|
|
428
|
-
billing: finalResponse.billing,
|
|
429
|
-
auxiliary: finalResponse.auxiliary,
|
|
430
|
-
warnings: finalResponse.warnings,
|
|
431
|
-
});
|
|
432
|
-
} finally {
|
|
433
|
-
this.activeStream = false;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private finalizeTurn(
|
|
438
|
-
request: NormalizedRequest,
|
|
439
|
-
factory: EventFactory,
|
|
440
|
-
mockRequest: MockProviderRequest,
|
|
441
|
-
output: OutputItem[],
|
|
442
|
-
completion: MockCompleteStep,
|
|
443
|
-
stepCount: number,
|
|
444
|
-
) {
|
|
445
|
-
const replay = completion.replay ?? replayFromOutput(output);
|
|
446
|
-
const toolCalls = output.filter((item): item is ToolCallItem => item.type === "tool_call");
|
|
447
|
-
|
|
448
|
-
this.previousReplay = replay;
|
|
449
|
-
this.pendingToolCalls = [...mockRequest.remainingPendingToolCalls, ...toolCalls];
|
|
450
|
-
this.history.push({
|
|
451
|
-
turnIndex: mockRequest.turnIndex,
|
|
452
|
-
requestId: request.requestId,
|
|
453
|
-
replay,
|
|
454
|
-
toolCalls,
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
return this.buildResponse(
|
|
458
|
-
request,
|
|
459
|
-
{
|
|
460
|
-
output,
|
|
461
|
-
replay,
|
|
462
|
-
stopReason: completion.stopReason ?? resolveStopReason(output),
|
|
463
|
-
usage: completion.usage,
|
|
464
|
-
billing: completion.billing,
|
|
465
|
-
auxiliary: completion.auxiliary,
|
|
466
|
-
providerMetadata: {
|
|
467
|
-
turnIndex: mockRequest.turnIndex,
|
|
468
|
-
stepCount,
|
|
469
|
-
pendingToolCallIds: this.pendingToolCalls.map((item) => item.id),
|
|
470
|
-
historyLength: this.history.length,
|
|
471
|
-
...this.providerMetadata,
|
|
472
|
-
...completion.providerMetadata,
|
|
473
|
-
},
|
|
474
|
-
warnings: completion.warnings,
|
|
475
|
-
metadataSources: ["mock"],
|
|
476
|
-
rawResponseId: completion.rawResponseId,
|
|
477
|
-
},
|
|
478
|
-
factory,
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private buildHandlerContext(turnIndex: number, request: NormalizedRequest): MockHandlerContext {
|
|
483
|
-
return {
|
|
484
|
-
turnIndex,
|
|
485
|
-
previousReplay: this.previousReplay.map(cloneItem),
|
|
486
|
-
pendingToolCalls: this.pendingToolCalls.map(cloneItem),
|
|
487
|
-
history: this.history.map((record) => ({
|
|
488
|
-
...record,
|
|
489
|
-
replay: record.replay.map(cloneItem),
|
|
490
|
-
toolCalls: record.toolCalls.map(cloneItem),
|
|
491
|
-
})),
|
|
492
|
-
signal: request.signal,
|
|
493
|
-
reasoningLevel: request.reasoningLevel,
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export function withMockStreaming(handler: MockStaticHandler, options: MockTextStreamOptions): MockHandler {
|
|
499
|
-
const defaults = resolveMockTextStreamOptions(options, "mock stream wrapper");
|
|
500
|
-
if (!defaults) {
|
|
501
|
-
throw new AIRequestError("mock stream wrapper requires streaming options", "MOCK_STREAM_CONFIG_INVALID");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return async function* streamWrappedHandler(
|
|
505
|
-
request: NormalizedRequest,
|
|
506
|
-
context: MockHandlerContext,
|
|
507
|
-
): AsyncIterable<MockStep> {
|
|
508
|
-
const source = await handler(request, context);
|
|
509
|
-
|
|
510
|
-
for await (const step of source) {
|
|
511
|
-
yield applyDefaultStreaming(step, defaults);
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function applyDefaultStreaming(step: MockStep, defaults: ResolvedMockTextStreamOptions): MockStep {
|
|
517
|
-
switch (step.type) {
|
|
518
|
-
case "message":
|
|
519
|
-
case "reasoning":
|
|
520
|
-
case "tool_call":
|
|
521
|
-
case "output":
|
|
522
|
-
if (step.stream !== undefined) {
|
|
523
|
-
return step;
|
|
524
|
-
}
|
|
525
|
-
return {
|
|
526
|
-
...step,
|
|
527
|
-
stream: {
|
|
528
|
-
charsPerSecond: defaults.charsPerSecond,
|
|
529
|
-
chunkSize: defaults.chunkSize,
|
|
530
|
-
initialDelayMs: defaults.initialDelayMs,
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
default:
|
|
534
|
-
return step;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function createMessageFromStep(
|
|
539
|
-
step: MockMessageStep,
|
|
540
|
-
request: NormalizedRequest,
|
|
541
|
-
turnIndex: number,
|
|
542
|
-
stepIndex: number,
|
|
543
|
-
): MessageItem {
|
|
544
|
-
return {
|
|
545
|
-
...messageItem(normalizeBlocks(step.content), {
|
|
546
|
-
id: step.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}`,
|
|
547
|
-
}),
|
|
548
|
-
role: "assistant",
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function createReasoningFromStep(
|
|
553
|
-
step: MockReasoningStep,
|
|
554
|
-
request: NormalizedRequest,
|
|
555
|
-
turnIndex: number,
|
|
556
|
-
stepIndex: number,
|
|
557
|
-
): Extract<OutputItem, { type: "reasoning" }> {
|
|
558
|
-
return reasoningItem(
|
|
559
|
-
normalizeBlocks(step.content),
|
|
560
|
-
step.visibility ?? "full",
|
|
561
|
-
step.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`,
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function createToolCallFromStep(step: MockToolCallStep): ToolCallItem {
|
|
566
|
-
return {
|
|
567
|
-
type: "tool_call",
|
|
568
|
-
id: step.id,
|
|
569
|
-
name: step.name,
|
|
570
|
-
argumentsText: step.argumentsText,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function normalizeBlocks(content: string | ContentBlock[]): ContentBlock[] {
|
|
575
|
-
return typeof content === "string" ? [textBlock(content)] : content;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function assertSupportedOutputItem(item: OutputItem): void {
|
|
579
|
-
if (item.type === "opaque") {
|
|
580
|
-
throw new AIRequestError(
|
|
581
|
-
"MockAdapter does not stream opaque output items; use complete.replay if needed",
|
|
582
|
-
"MOCK_OPAQUE_OUTPUT",
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function attachSyntheticId(
|
|
588
|
-
item: Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }>,
|
|
589
|
-
request: NormalizedRequest,
|
|
590
|
-
turnIndex: number,
|
|
591
|
-
stepIndex: number,
|
|
592
|
-
): Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }> {
|
|
593
|
-
if (item.type === "message") {
|
|
594
|
-
return {
|
|
595
|
-
...item,
|
|
596
|
-
id: item.id ?? `mock-msg-${request.requestId}-${turnIndex}-${stepIndex}`,
|
|
597
|
-
role: "assistant",
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (item.type === "reasoning") {
|
|
602
|
-
return {
|
|
603
|
-
...item,
|
|
604
|
-
id: item.id ?? `mock-reason-${request.requestId}-${turnIndex}-${stepIndex}`,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
return item;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function* emitOutputItem(
|
|
612
|
-
factory: EventFactory,
|
|
613
|
-
item: Extract<OutputItem, { type: "message" | "reasoning" | "tool_call" }>,
|
|
614
|
-
stream?: ResolvedMockTextStreamOptions,
|
|
615
|
-
): AsyncIterable<AIStreamEvent> {
|
|
616
|
-
if (item.type === "message") {
|
|
617
|
-
yield* emitMessage(factory, item, stream);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (item.type === "reasoning") {
|
|
622
|
-
yield* emitReasoning(factory, item, stream);
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
yield* emitToolCall(factory, item, true, stream);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async function* emitMessage(
|
|
630
|
-
factory: EventFactory,
|
|
631
|
-
item: MessageItem,
|
|
632
|
-
stream?: ResolvedMockTextStreamOptions,
|
|
633
|
-
): AsyncIterable<AIStreamEvent> {
|
|
634
|
-
if (!item.id) {
|
|
635
|
-
throw new AIRequestError("Mock message output requires an id after normalization", "MOCK_MESSAGE_ID_MISSING");
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
yield factory.messageStarted(item.id);
|
|
639
|
-
|
|
640
|
-
let chunkIndex = 0;
|
|
641
|
-
for (const block of item.content) {
|
|
642
|
-
if (block.type === "text") {
|
|
643
|
-
for (const chunk of chunkText(block.text, stream)) {
|
|
644
|
-
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
645
|
-
yield factory.messageDelta(item.id, textBlock(chunk));
|
|
646
|
-
chunkIndex += 1;
|
|
647
|
-
}
|
|
648
|
-
} else {
|
|
649
|
-
yield factory.messageDelta(item.id, block);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
yield factory.messageCompleted(item.id);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
async function* emitReasoning(
|
|
657
|
-
factory: EventFactory,
|
|
658
|
-
item: Extract<OutputItem, { type: "reasoning" }>,
|
|
659
|
-
stream?: ResolvedMockTextStreamOptions,
|
|
660
|
-
): AsyncIterable<AIStreamEvent> {
|
|
661
|
-
if (!item.id) {
|
|
662
|
-
throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
yield factory.reasoningStarted(item.id, item.visibility);
|
|
666
|
-
|
|
667
|
-
let chunkIndex = 0;
|
|
668
|
-
for (const block of item.content) {
|
|
669
|
-
if (block.type !== "text") {
|
|
670
|
-
yield factory.reasoningDelta(item.id, block);
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
for (const chunk of chunkText(block.text, stream)) {
|
|
675
|
-
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
676
|
-
yield factory.reasoningDelta(item.id, textBlock(chunk));
|
|
677
|
-
chunkIndex += 1;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
yield factory.reasoningCompleted(item.id);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
async function* emitToolCall(
|
|
685
|
-
factory: EventFactory,
|
|
686
|
-
item: ToolCallItem,
|
|
687
|
-
streamArguments: boolean,
|
|
688
|
-
stream?: ResolvedMockTextStreamOptions,
|
|
689
|
-
): AsyncIterable<AIStreamEvent> {
|
|
690
|
-
yield factory.toolCallStarted(item.id, item.name);
|
|
691
|
-
|
|
692
|
-
if (streamArguments && item.argumentsText) {
|
|
693
|
-
let chunkIndex = 0;
|
|
694
|
-
for (const chunk of chunkText(item.argumentsText, stream)) {
|
|
695
|
-
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
696
|
-
yield factory.toolCallDelta(item.id, { argumentsText: chunk });
|
|
697
|
-
chunkIndex += 1;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
yield factory.toolCallCompleted(item.id);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function resolveStepStreamOptions(
|
|
705
|
-
defaults: ResolvedMockTextStreamOptions | undefined,
|
|
706
|
-
override: MockTextStreamOptions | false | undefined,
|
|
707
|
-
label: string,
|
|
708
|
-
): ResolvedMockTextStreamOptions | undefined {
|
|
709
|
-
if (override === false) {
|
|
710
|
-
return undefined;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return resolveMockTextStreamOptions(override, `${label} stream`, defaults);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function resolveMockTextStreamOptions(
|
|
717
|
-
options: MockTextStreamOptions | undefined,
|
|
718
|
-
label: string,
|
|
719
|
-
defaults?: ResolvedMockTextStreamOptions,
|
|
720
|
-
): ResolvedMockTextStreamOptions | undefined {
|
|
721
|
-
if (options === undefined) {
|
|
722
|
-
return defaults;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const chunkSize = options.chunkSize ?? defaults?.chunkSize ?? 1;
|
|
726
|
-
const initialDelayMs = options.initialDelayMs ?? defaults?.initialDelayMs ?? 0;
|
|
727
|
-
const charsPerSecond = options.charsPerSecond ?? defaults?.charsPerSecond;
|
|
728
|
-
|
|
729
|
-
if (!Number.isInteger(chunkSize) || chunkSize < 1) {
|
|
730
|
-
throw new AIRequestError(`${label}: chunkSize must be a positive integer`, "MOCK_STREAM_CONFIG_INVALID");
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (!Number.isFinite(initialDelayMs) || initialDelayMs < 0) {
|
|
734
|
-
throw new AIRequestError(`${label}: initialDelayMs must be a non-negative number`, "MOCK_STREAM_CONFIG_INVALID");
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (charsPerSecond !== undefined && (!Number.isFinite(charsPerSecond) || charsPerSecond <= 0)) {
|
|
738
|
-
throw new AIRequestError(`${label}: charsPerSecond must be a positive number`, "MOCK_STREAM_CONFIG_INVALID");
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return {
|
|
742
|
-
chunkSize,
|
|
743
|
-
initialDelayMs,
|
|
744
|
-
charsPerSecond,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function chunkText(text: string, stream?: ResolvedMockTextStreamOptions): string[] {
|
|
749
|
-
if (!text) {
|
|
750
|
-
return [];
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (!stream) {
|
|
754
|
-
return [text];
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const chars = Array.from(text);
|
|
758
|
-
const chunks: string[] = [];
|
|
759
|
-
|
|
760
|
-
for (let index = 0; index < chars.length; index += stream.chunkSize) {
|
|
761
|
-
chunks.push(chars.slice(index, index + stream.chunkSize).join(""));
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
return chunks;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
async function delayForChunk(
|
|
768
|
-
stream: ResolvedMockTextStreamOptions | undefined,
|
|
769
|
-
chunkIndex: number,
|
|
770
|
-
chunkLength: number,
|
|
771
|
-
): Promise<void> {
|
|
772
|
-
if (!stream) {
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (chunkIndex === 0 && stream.initialDelayMs > 0) {
|
|
777
|
-
await sleep(stream.initialDelayMs);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (chunkIndex > 0 && stream.charsPerSecond !== undefined) {
|
|
782
|
-
await sleep((chunkLength / stream.charsPerSecond) * 1000);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async function sleep(ms: number): Promise<void> {
|
|
787
|
-
if (ms <= 0) {
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function resolveStopReason(output: OutputItem[]): StopReason {
|
|
795
|
-
return output.some((item) => item.type === "tool_call") ? "tool_call" : "end_turn";
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function consumePendingToolCalls(pending: readonly ToolCallItem[], input: readonly InputItem[]): ToolCallItem[] {
|
|
799
|
-
const fulfilledIds = new Set(
|
|
800
|
-
input.filter((item): item is ToolResultItem => item.type === "tool_result").map((item) => item.callId),
|
|
801
|
-
);
|
|
802
|
-
|
|
803
|
-
return pending.filter((item) => !fulfilledIds.has(item.id)).map(cloneItem);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function assertReplayIncluded(input: readonly InputItem[], replay: readonly ReplayItem[], prefix: string): void {
|
|
807
|
-
const fingerprints = input.map(fingerprintItem);
|
|
808
|
-
let cursor = 0;
|
|
809
|
-
|
|
810
|
-
for (const replayItem of replay) {
|
|
811
|
-
const target = fingerprintItem(replayItem);
|
|
812
|
-
const foundIndex = fingerprints.indexOf(target, cursor);
|
|
813
|
-
if (foundIndex === -1) {
|
|
814
|
-
throw new AIRequestError(
|
|
815
|
-
`${prefix}: previous replay item was not carried into the next request`,
|
|
816
|
-
"MOCK_EXPECTATION_FAILED",
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
cursor = foundIndex + 1;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function assertOrderedItems(
|
|
824
|
-
input: readonly InputItem[],
|
|
825
|
-
expectations: readonly MockInputExpectation[],
|
|
826
|
-
prefix: string,
|
|
827
|
-
): void {
|
|
828
|
-
let cursor = 0;
|
|
829
|
-
|
|
830
|
-
for (const expected of expectations) {
|
|
831
|
-
let matched = false;
|
|
832
|
-
while (cursor < input.length) {
|
|
833
|
-
const item = input[cursor];
|
|
834
|
-
if (item !== undefined && matchesItemExpectation(item, expected)) {
|
|
835
|
-
matched = true;
|
|
836
|
-
cursor += 1;
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
839
|
-
cursor += 1;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (!matched) {
|
|
843
|
-
throw new AIRequestError(
|
|
844
|
-
`${prefix}: missing ordered input item ${describeExpectation(expected)}`,
|
|
845
|
-
"MOCK_EXPECTATION_FAILED",
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function assertUnorderedItems(
|
|
852
|
-
input: readonly InputItem[],
|
|
853
|
-
expectations: readonly MockInputExpectation[],
|
|
854
|
-
prefix: string,
|
|
855
|
-
): void {
|
|
856
|
-
for (const expected of expectations) {
|
|
857
|
-
const matched = input.some((item) => matchesItemExpectation(item, expected));
|
|
858
|
-
if (!matched) {
|
|
859
|
-
throw new AIRequestError(
|
|
860
|
-
`${prefix}: missing input item ${describeExpectation(expected)}`,
|
|
861
|
-
"MOCK_EXPECTATION_FAILED",
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function matchesItemExpectation(item: InputItem, expected: MockInputExpectation): boolean {
|
|
868
|
-
if (item.type !== expected.type) {
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (expected.id !== undefined && "id" in item && item.id !== expected.id) {
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
switch (item.type) {
|
|
877
|
-
case "message":
|
|
878
|
-
return (
|
|
879
|
-
(expected.role === undefined || item.role === expected.role) && matchesText(item.content, expected.textIncludes)
|
|
880
|
-
);
|
|
881
|
-
case "reasoning":
|
|
882
|
-
return (
|
|
883
|
-
(expected.visibility === undefined || item.visibility === expected.visibility) &&
|
|
884
|
-
matchesText(item.content, expected.textIncludes)
|
|
885
|
-
);
|
|
886
|
-
case "tool_call":
|
|
887
|
-
return (
|
|
888
|
-
(expected.name === undefined || item.name === expected.name) &&
|
|
889
|
-
(expected.textIncludes === undefined || item.argumentsText.includes(expected.textIncludes))
|
|
890
|
-
);
|
|
891
|
-
case "tool_result":
|
|
892
|
-
return (
|
|
893
|
-
(expected.toolName === undefined || item.toolName === expected.toolName) &&
|
|
894
|
-
(expected.callId === undefined || item.callId === expected.callId) &&
|
|
895
|
-
(expected.outcome === undefined || item.outcome === expected.outcome) &&
|
|
896
|
-
matchesText(item.content, expected.textIncludes)
|
|
897
|
-
);
|
|
898
|
-
case "opaque":
|
|
899
|
-
return (
|
|
900
|
-
(expected.source === undefined || item.source === expected.source) &&
|
|
901
|
-
(expected.purpose === undefined || item.purpose === expected.purpose)
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function matchesText(blocks: readonly ContentBlock[], textIncludes: string | undefined): boolean {
|
|
907
|
-
if (textIncludes === undefined) {
|
|
908
|
-
return true;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
return blocks.some((block) => {
|
|
912
|
-
if (block.type === "text") return block.text.includes(textIncludes);
|
|
913
|
-
if (block.type === "json") return JSON.stringify(block.json).includes(textIncludes);
|
|
914
|
-
return false;
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function fingerprintItem(item: InputItem): string {
|
|
919
|
-
return JSON.stringify(item);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function describeExpectation(expectation: MockInputExpectation): string {
|
|
923
|
-
const parts = [`type=${expectation.type}`];
|
|
924
|
-
if (expectation.role) parts.push(`role=${expectation.role}`);
|
|
925
|
-
if (expectation.name) parts.push(`name=${expectation.name}`);
|
|
926
|
-
if (expectation.toolName) parts.push(`toolName=${expectation.toolName}`);
|
|
927
|
-
if (expectation.callId) parts.push(`callId=${expectation.callId}`);
|
|
928
|
-
if (expectation.textIncludes) parts.push(`textIncludes=${JSON.stringify(expectation.textIncludes)}`);
|
|
929
|
-
return `{ ${parts.join(", ")} }`;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function cloneItem<T>(item: T): T {
|
|
933
|
-
return structuredClone(item);
|
|
934
|
-
}
|