llmist 1.3.0 → 1.4.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.
@@ -1,1394 +0,0 @@
1
- import {
2
- BaseGadget,
3
- GADGET_ARG_PREFIX,
4
- GADGET_END_PREFIX,
5
- GADGET_START_PREFIX,
6
- LLMist,
7
- createLogger,
8
- init_client,
9
- init_constants,
10
- init_gadget,
11
- init_logger
12
- } from "./chunk-RZTAKIDE.js";
13
-
14
- // src/gadgets/validation.ts
15
- function validateAndApplyDefaults(schema, params) {
16
- const result = schema.safeParse(params);
17
- if (result.success) {
18
- return {
19
- success: true,
20
- data: result.data
21
- };
22
- }
23
- const issues = result.error.issues.map((issue) => ({
24
- path: issue.path.join(".") || "root",
25
- message: issue.message
26
- }));
27
- const formattedError = `Invalid parameters: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`;
28
- return {
29
- success: false,
30
- error: formattedError,
31
- issues
32
- };
33
- }
34
- function validateGadgetParams(gadget, params) {
35
- if (!gadget.parameterSchema) {
36
- return {
37
- success: true,
38
- data: params
39
- };
40
- }
41
- return validateAndApplyDefaults(gadget.parameterSchema, params);
42
- }
43
-
44
- // src/testing/gadget-testing.ts
45
- async function testGadget(gadget, params, options) {
46
- let validatedParams = params;
47
- if (!options?.skipValidation) {
48
- const validationResult = validateGadgetParams(gadget, params);
49
- if (!validationResult.success) {
50
- return {
51
- error: validationResult.error,
52
- validatedParams: params
53
- };
54
- }
55
- validatedParams = validationResult.data;
56
- }
57
- try {
58
- const result = await Promise.resolve(gadget.execute(validatedParams));
59
- return {
60
- result,
61
- validatedParams
62
- };
63
- } catch (error) {
64
- return {
65
- error: error instanceof Error ? error.message : String(error),
66
- validatedParams
67
- };
68
- }
69
- }
70
- async function testGadgetBatch(gadget, paramSets, options) {
71
- return Promise.all(paramSets.map((params) => testGadget(gadget, params, options)));
72
- }
73
-
74
- // src/testing/mock-manager.ts
75
- init_logger();
76
- var MockManager = class _MockManager {
77
- static instance = null;
78
- mocks = /* @__PURE__ */ new Map();
79
- stats = /* @__PURE__ */ new Map();
80
- options;
81
- logger;
82
- nextId = 1;
83
- constructor(options = {}) {
84
- this.options = {
85
- strictMode: options.strictMode ?? false,
86
- debug: options.debug ?? false,
87
- recordStats: options.recordStats ?? true
88
- };
89
- this.logger = createLogger({ name: "MockManager", minLevel: this.options.debug ? 2 : 3 });
90
- }
91
- /**
92
- * Get the global MockManager instance.
93
- * Creates one if it doesn't exist.
94
- */
95
- static getInstance(options) {
96
- if (!_MockManager.instance) {
97
- _MockManager.instance = new _MockManager(options);
98
- } else if (options) {
99
- console.warn(
100
- "MockManager.getInstance() called with options, but instance already exists. Options are ignored. Use setOptions() to update options or reset() to reinitialize."
101
- );
102
- }
103
- return _MockManager.instance;
104
- }
105
- /**
106
- * Reset the global instance (useful for testing).
107
- */
108
- static reset() {
109
- _MockManager.instance = null;
110
- }
111
- /**
112
- * Register a new mock.
113
- *
114
- * @param registration - The mock registration configuration
115
- * @returns The ID of the registered mock
116
- *
117
- * @example
118
- * const manager = MockManager.getInstance();
119
- * const mockId = manager.register({
120
- * label: 'GPT-4 mock',
121
- * matcher: (ctx) => ctx.modelName.includes('gpt-4'),
122
- * response: { text: 'Mocked response' }
123
- * });
124
- */
125
- register(registration) {
126
- const id = registration.id ?? `mock-${this.nextId++}`;
127
- const mock = {
128
- id,
129
- matcher: registration.matcher,
130
- response: registration.response,
131
- label: registration.label,
132
- once: registration.once
133
- };
134
- this.mocks.set(id, mock);
135
- if (this.options.recordStats) {
136
- this.stats.set(id, { matchCount: 0 });
137
- }
138
- this.logger.debug(
139
- `Registered mock: ${id}${mock.label ? ` (${mock.label})` : ""}${mock.once ? " [once]" : ""}`
140
- );
141
- return id;
142
- }
143
- /**
144
- * Unregister a mock by ID.
145
- */
146
- unregister(id) {
147
- const deleted = this.mocks.delete(id);
148
- if (deleted) {
149
- this.stats.delete(id);
150
- this.logger.debug(`Unregistered mock: ${id}`);
151
- }
152
- return deleted;
153
- }
154
- /**
155
- * Clear all registered mocks.
156
- */
157
- clear() {
158
- this.mocks.clear();
159
- this.stats.clear();
160
- this.logger.debug("Cleared all mocks");
161
- }
162
- /**
163
- * Find and return a matching mock for the given context.
164
- * Returns the mock response if found, null otherwise.
165
- */
166
- async findMatch(context) {
167
- this.logger.debug(
168
- `Finding match for: ${context.provider}:${context.modelName} (${this.mocks.size} mocks registered)`
169
- );
170
- for (const [id, mock] of this.mocks.entries()) {
171
- let matches = false;
172
- try {
173
- matches = await Promise.resolve(mock.matcher(context));
174
- } catch (error) {
175
- this.logger.warn(`Error in matcher ${id}:`, error);
176
- if (this.options.strictMode) {
177
- throw new Error(`Matcher error in mock ${id}: ${error}`);
178
- }
179
- continue;
180
- }
181
- if (matches) {
182
- this.logger.debug(`Mock matched: ${id}${mock.label ? ` (${mock.label})` : ""}`);
183
- if (this.options.recordStats) {
184
- const stats = this.stats.get(id);
185
- if (stats) {
186
- stats.matchCount++;
187
- stats.lastUsed = /* @__PURE__ */ new Date();
188
- }
189
- }
190
- if (mock.once) {
191
- this.mocks.delete(id);
192
- this.stats.delete(id);
193
- this.logger.debug(`Removed one-time mock: ${id}`);
194
- }
195
- const response = typeof mock.response === "function" ? await Promise.resolve(mock.response(context)) : mock.response;
196
- return response;
197
- }
198
- }
199
- this.logger.debug("No mock matched");
200
- if (this.options.strictMode) {
201
- throw new Error(
202
- `No mock registered for ${context.provider}:${context.modelName}. Register a mock using MockManager.getInstance().register() or disable strictMode.`
203
- );
204
- }
205
- return {
206
- text: "",
207
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
208
- finishReason: "stop"
209
- };
210
- }
211
- /**
212
- * Get statistics for a specific mock.
213
- */
214
- getStats(id) {
215
- return this.stats.get(id);
216
- }
217
- /**
218
- * Get all registered mock IDs.
219
- */
220
- getMockIds() {
221
- return Array.from(this.mocks.keys());
222
- }
223
- /**
224
- * Get the number of registered mocks.
225
- */
226
- getCount() {
227
- return this.mocks.size;
228
- }
229
- /**
230
- * Update the mock manager options.
231
- */
232
- setOptions(options) {
233
- this.options = { ...this.options, ...options };
234
- this.logger = createLogger({ name: "MockManager", minLevel: this.options.debug ? 2 : 3 });
235
- }
236
- };
237
- function getMockManager(options) {
238
- return MockManager.getInstance(options);
239
- }
240
-
241
- // src/testing/mock-stream.ts
242
- init_constants();
243
- function sleep(ms) {
244
- return new Promise((resolve) => setTimeout(resolve, ms));
245
- }
246
- function generateInvocationId() {
247
- return `inv-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
248
- }
249
- function splitIntoChunks(text, minChunkSize = 5, maxChunkSize = 30) {
250
- const chunks = [];
251
- let remaining = text;
252
- while (remaining.length > 0) {
253
- const chunkSize = Math.min(
254
- Math.floor(Math.random() * (maxChunkSize - minChunkSize + 1)) + minChunkSize,
255
- remaining.length
256
- );
257
- let chunk;
258
- if (chunkSize < remaining.length) {
259
- const substr = remaining.substring(0, chunkSize);
260
- const lastSpace = substr.lastIndexOf(" ");
261
- if (lastSpace > minChunkSize / 2) {
262
- chunk = substr.substring(0, lastSpace + 1);
263
- } else {
264
- chunk = substr;
265
- }
266
- } else {
267
- chunk = remaining;
268
- }
269
- chunks.push(chunk);
270
- remaining = remaining.substring(chunk.length);
271
- }
272
- return chunks;
273
- }
274
- function serializeToBlockFormat(obj, prefix = "") {
275
- let result = "";
276
- for (const [key, value] of Object.entries(obj)) {
277
- const pointer = prefix ? `${prefix}/${key}` : key;
278
- if (value === null || value === void 0) {
279
- continue;
280
- }
281
- if (Array.isArray(value)) {
282
- for (let i = 0; i < value.length; i++) {
283
- const item = value[i];
284
- const itemPointer = `${pointer}/${i}`;
285
- if (typeof item === "object" && item !== null && !Array.isArray(item)) {
286
- result += serializeToBlockFormat(item, itemPointer);
287
- } else if (Array.isArray(item)) {
288
- for (let j = 0; j < item.length; j++) {
289
- result += `${GADGET_ARG_PREFIX}${itemPointer}/${j}
290
- ${String(item[j])}
291
- `;
292
- }
293
- } else {
294
- result += `${GADGET_ARG_PREFIX}${itemPointer}
295
- ${String(item)}
296
- `;
297
- }
298
- }
299
- } else if (typeof value === "object") {
300
- result += serializeToBlockFormat(value, pointer);
301
- } else {
302
- result += `${GADGET_ARG_PREFIX}${pointer}
303
- ${String(value)}
304
- `;
305
- }
306
- }
307
- return result;
308
- }
309
- function formatGadgetCalls(gadgetCalls) {
310
- let text = "";
311
- const calls = [];
312
- for (const call of gadgetCalls) {
313
- const invocationId = call.invocationId ?? generateInvocationId();
314
- calls.push({ name: call.gadgetName, invocationId });
315
- const blockParams = serializeToBlockFormat(call.parameters);
316
- text += `
317
- ${GADGET_START_PREFIX}${call.gadgetName}
318
- ${blockParams}${GADGET_END_PREFIX}`;
319
- }
320
- return { text, calls };
321
- }
322
- async function* createMockStream(response) {
323
- if (response.delayMs) {
324
- await sleep(response.delayMs);
325
- }
326
- const streamDelay = response.streamDelayMs ?? 0;
327
- let fullText = response.text ?? "";
328
- if (response.gadgetCalls && response.gadgetCalls.length > 0) {
329
- const { text: gadgetText } = formatGadgetCalls(response.gadgetCalls);
330
- fullText += gadgetText;
331
- }
332
- if (fullText.length > 0) {
333
- const chunks = streamDelay > 0 ? splitIntoChunks(fullText) : [fullText];
334
- for (let i = 0; i < chunks.length; i++) {
335
- const isLast = i === chunks.length - 1;
336
- const chunk = {
337
- text: chunks[i]
338
- };
339
- if (isLast) {
340
- if (response.finishReason !== void 0) {
341
- chunk.finishReason = response.finishReason;
342
- }
343
- if (response.usage) {
344
- chunk.usage = response.usage;
345
- }
346
- }
347
- yield chunk;
348
- if (streamDelay > 0 && !isLast) {
349
- await sleep(streamDelay);
350
- }
351
- }
352
- } else {
353
- yield {
354
- text: "",
355
- finishReason: response.finishReason ?? "stop",
356
- usage: response.usage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
357
- };
358
- }
359
- }
360
- function createTextMockStream(text, options) {
361
- return createMockStream({
362
- text,
363
- delayMs: options?.delayMs,
364
- streamDelayMs: options?.streamDelayMs,
365
- usage: options?.usage,
366
- finishReason: "stop"
367
- });
368
- }
369
-
370
- // src/testing/mock-adapter.ts
371
- var MockProviderAdapter = class {
372
- providerId = "mock";
373
- priority = 100;
374
- // High priority: check mocks before real providers
375
- mockManager;
376
- constructor(options) {
377
- this.mockManager = getMockManager(options);
378
- }
379
- supports(descriptor) {
380
- return true;
381
- }
382
- stream(options, descriptor, spec) {
383
- const context = {
384
- model: options.model,
385
- provider: descriptor.provider,
386
- modelName: descriptor.name,
387
- options,
388
- messages: options.messages
389
- };
390
- return this.createMockStreamFromContext(context);
391
- }
392
- async *createMockStreamFromContext(context) {
393
- try {
394
- const mockResponse = await this.mockManager.findMatch(context);
395
- if (!mockResponse) {
396
- yield {
397
- text: "",
398
- finishReason: "stop",
399
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
400
- };
401
- return;
402
- }
403
- yield* createMockStream(mockResponse);
404
- } catch (error) {
405
- throw error;
406
- }
407
- }
408
- };
409
- function createMockAdapter(options) {
410
- return new MockProviderAdapter(options);
411
- }
412
-
413
- // src/testing/mock-builder.ts
414
- var MockBuilder = class {
415
- matchers = [];
416
- response = {};
417
- label;
418
- isOnce = false;
419
- id;
420
- /**
421
- * Match calls to a specific model (by name, supports partial matching).
422
- *
423
- * @example
424
- * mockLLM().forModel('gpt-5')
425
- * mockLLM().forModel('claude') // matches any Claude model
426
- */
427
- forModel(modelName) {
428
- if (!modelName || modelName.trim() === "") {
429
- throw new Error("Model name cannot be empty");
430
- }
431
- this.matchers.push((ctx) => ctx.modelName.includes(modelName));
432
- return this;
433
- }
434
- /**
435
- * Match calls to any model.
436
- * Useful when you want to mock responses regardless of the model used.
437
- *
438
- * @example
439
- * mockLLM().forAnyModel()
440
- */
441
- forAnyModel() {
442
- this.matchers.push(() => true);
443
- return this;
444
- }
445
- /**
446
- * Match calls to a specific provider.
447
- *
448
- * @example
449
- * mockLLM().forProvider('openai')
450
- * mockLLM().forProvider('anthropic')
451
- */
452
- forProvider(provider) {
453
- if (!provider || provider.trim() === "") {
454
- throw new Error("Provider name cannot be empty");
455
- }
456
- this.matchers.push((ctx) => ctx.provider === provider);
457
- return this;
458
- }
459
- /**
460
- * Match calls to any provider.
461
- * Useful when you want to mock responses regardless of the provider used.
462
- *
463
- * @example
464
- * mockLLM().forAnyProvider()
465
- */
466
- forAnyProvider() {
467
- this.matchers.push(() => true);
468
- return this;
469
- }
470
- /**
471
- * Match when any message contains the given text (case-insensitive).
472
- *
473
- * @example
474
- * mockLLM().whenMessageContains('hello')
475
- */
476
- whenMessageContains(text) {
477
- this.matchers.push(
478
- (ctx) => ctx.messages.some((msg) => msg.content?.toLowerCase().includes(text.toLowerCase()))
479
- );
480
- return this;
481
- }
482
- /**
483
- * Match when the last message contains the given text (case-insensitive).
484
- *
485
- * @example
486
- * mockLLM().whenLastMessageContains('goodbye')
487
- */
488
- whenLastMessageContains(text) {
489
- this.matchers.push((ctx) => {
490
- const lastMsg = ctx.messages[ctx.messages.length - 1];
491
- return lastMsg?.content?.toLowerCase().includes(text.toLowerCase()) ?? false;
492
- });
493
- return this;
494
- }
495
- /**
496
- * Match when any message matches the given regex.
497
- *
498
- * @example
499
- * mockLLM().whenMessageMatches(/calculate \d+/)
500
- */
501
- whenMessageMatches(regex) {
502
- this.matchers.push((ctx) => ctx.messages.some((msg) => regex.test(msg.content ?? "")));
503
- return this;
504
- }
505
- /**
506
- * Match when a message with a specific role contains text.
507
- *
508
- * @example
509
- * mockLLM().whenRoleContains('system', 'You are a helpful assistant')
510
- */
511
- whenRoleContains(role, text) {
512
- this.matchers.push(
513
- (ctx) => ctx.messages.some(
514
- (msg) => msg.role === role && msg.content?.toLowerCase().includes(text.toLowerCase())
515
- )
516
- );
517
- return this;
518
- }
519
- /**
520
- * Match based on the number of messages in the conversation.
521
- *
522
- * @example
523
- * mockLLM().whenMessageCount((count) => count > 10)
524
- */
525
- whenMessageCount(predicate) {
526
- this.matchers.push((ctx) => predicate(ctx.messages.length));
527
- return this;
528
- }
529
- /**
530
- * Add a custom matcher function.
531
- * This provides full control over matching logic.
532
- *
533
- * @example
534
- * mockLLM().when((ctx) => {
535
- * return ctx.options.temperature > 0.8;
536
- * })
537
- */
538
- when(matcher) {
539
- this.matchers.push(matcher);
540
- return this;
541
- }
542
- /**
543
- * Set the text response to return.
544
- * Can be a static string or a function that returns a string dynamically.
545
- *
546
- * @example
547
- * mockLLM().returns('Hello, world!')
548
- * mockLLM().returns(() => `Response at ${Date.now()}`)
549
- * mockLLM().returns((ctx) => `You said: ${ctx.messages[0]?.content}`)
550
- */
551
- returns(text) {
552
- if (typeof text === "function") {
553
- this.response = async (ctx) => {
554
- const resolvedText = await Promise.resolve().then(() => text(ctx));
555
- return { text: resolvedText };
556
- };
557
- } else {
558
- if (typeof this.response === "function") {
559
- throw new Error("Cannot use returns() after withResponse() with a function");
560
- }
561
- this.response.text = text;
562
- }
563
- return this;
564
- }
565
- /**
566
- * Set gadget calls to include in the response.
567
- *
568
- * @example
569
- * mockLLM().returnsGadgetCalls([
570
- * { gadgetName: 'calculator', parameters: { op: 'add', a: 1, b: 2 } }
571
- * ])
572
- */
573
- returnsGadgetCalls(calls) {
574
- if (typeof this.response === "function") {
575
- throw new Error("Cannot use returnsGadgetCalls() after withResponse() with a function");
576
- }
577
- this.response.gadgetCalls = calls;
578
- return this;
579
- }
580
- /**
581
- * Add a single gadget call to the response.
582
- *
583
- * @example
584
- * mockLLM()
585
- * .returnsGadgetCall('calculator', { op: 'add', a: 1, b: 2 })
586
- * .returnsGadgetCall('logger', { message: 'Done!' })
587
- */
588
- returnsGadgetCall(gadgetName, parameters) {
589
- if (typeof this.response === "function") {
590
- throw new Error("Cannot use returnsGadgetCall() after withResponse() with a function");
591
- }
592
- if (!this.response.gadgetCalls) {
593
- this.response.gadgetCalls = [];
594
- }
595
- this.response.gadgetCalls.push({ gadgetName, parameters });
596
- return this;
597
- }
598
- /**
599
- * Set the complete mock response object.
600
- * This allows full control over all response properties.
601
- * Can also be a function that generates the response dynamically based on context.
602
- *
603
- * @example
604
- * // Static response
605
- * mockLLM().withResponse({
606
- * text: 'Hello',
607
- * usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
608
- * finishReason: 'stop'
609
- * })
610
- *
611
- * @example
612
- * // Dynamic response
613
- * mockLLM().withResponse((ctx) => ({
614
- * text: `You said: ${ctx.messages[ctx.messages.length - 1]?.content}`,
615
- * usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
616
- * }))
617
- */
618
- withResponse(response) {
619
- this.response = response;
620
- return this;
621
- }
622
- /**
623
- * Set simulated token usage.
624
- *
625
- * @example
626
- * mockLLM().withUsage({ inputTokens: 100, outputTokens: 50, totalTokens: 150 })
627
- */
628
- withUsage(usage) {
629
- if (typeof this.response === "function") {
630
- throw new Error("Cannot use withUsage() after withResponse() with a function");
631
- }
632
- if (usage.inputTokens < 0 || usage.outputTokens < 0 || usage.totalTokens < 0) {
633
- throw new Error("Token counts cannot be negative");
634
- }
635
- if (usage.totalTokens !== usage.inputTokens + usage.outputTokens) {
636
- throw new Error("totalTokens must equal inputTokens + outputTokens");
637
- }
638
- this.response.usage = usage;
639
- return this;
640
- }
641
- /**
642
- * Set the finish reason.
643
- *
644
- * @example
645
- * mockLLM().withFinishReason('stop')
646
- * mockLLM().withFinishReason('length')
647
- */
648
- withFinishReason(reason) {
649
- if (typeof this.response === "function") {
650
- throw new Error("Cannot use withFinishReason() after withResponse() with a function");
651
- }
652
- this.response.finishReason = reason;
653
- return this;
654
- }
655
- /**
656
- * Set initial delay before streaming starts (simulates network latency).
657
- *
658
- * @example
659
- * mockLLM().withDelay(100) // 100ms delay
660
- */
661
- withDelay(ms) {
662
- if (typeof this.response === "function") {
663
- throw new Error("Cannot use withDelay() after withResponse() with a function");
664
- }
665
- if (ms < 0) {
666
- throw new Error("Delay must be non-negative");
667
- }
668
- this.response.delayMs = ms;
669
- return this;
670
- }
671
- /**
672
- * Set delay between stream chunks (simulates realistic streaming).
673
- *
674
- * @example
675
- * mockLLM().withStreamDelay(10) // 10ms between chunks
676
- */
677
- withStreamDelay(ms) {
678
- if (typeof this.response === "function") {
679
- throw new Error("Cannot use withStreamDelay() after withResponse() with a function");
680
- }
681
- if (ms < 0) {
682
- throw new Error("Stream delay must be non-negative");
683
- }
684
- this.response.streamDelayMs = ms;
685
- return this;
686
- }
687
- /**
688
- * Set a label for this mock (useful for debugging).
689
- *
690
- * @example
691
- * mockLLM().withLabel('greeting mock')
692
- */
693
- withLabel(label) {
694
- this.label = label;
695
- return this;
696
- }
697
- /**
698
- * Set a specific ID for this mock.
699
- *
700
- * @example
701
- * mockLLM().withId('my-custom-mock-id')
702
- */
703
- withId(id) {
704
- this.id = id;
705
- return this;
706
- }
707
- /**
708
- * Mark this mock as one-time use (will be removed after first match).
709
- *
710
- * @example
711
- * mockLLM().once()
712
- */
713
- once() {
714
- this.isOnce = true;
715
- return this;
716
- }
717
- /**
718
- * Build the mock registration without registering it.
719
- * Useful if you want to register it manually later.
720
- *
721
- * @returns The built MockRegistration object (without id if not specified)
722
- */
723
- build() {
724
- if (this.matchers.length === 0) {
725
- throw new Error(
726
- "Mock must have at least one matcher. Use .when(), .forModel(), .forProvider(), etc."
727
- );
728
- }
729
- const combinedMatcher = async (ctx) => {
730
- for (const matcher of this.matchers) {
731
- const matches = await Promise.resolve(matcher(ctx));
732
- if (!matches) return false;
733
- }
734
- return true;
735
- };
736
- return {
737
- id: this.id,
738
- matcher: combinedMatcher,
739
- response: this.response,
740
- label: this.label,
741
- once: this.isOnce
742
- };
743
- }
744
- /**
745
- * Register this mock with the global MockManager.
746
- * Returns the ID of the registered mock.
747
- *
748
- * @example
749
- * const mockId = mockLLM().forModel('gpt-5').returns('Hello!').register();
750
- * // Later: getMockManager().unregister(mockId);
751
- */
752
- register() {
753
- const mockManager = getMockManager();
754
- const registration = this.build();
755
- return mockManager.register(registration);
756
- }
757
- };
758
- function mockLLM() {
759
- return new MockBuilder();
760
- }
761
-
762
- // src/testing/mock-client.ts
763
- init_client();
764
- function createMockClient(options) {
765
- return new LLMist({
766
- adapters: [new MockProviderAdapter(options)],
767
- autoDiscoverProviders: false,
768
- defaultProvider: "mock"
769
- });
770
- }
771
-
772
- // src/testing/mock-gadget.ts
773
- init_gadget();
774
- var MockGadgetImpl = class extends BaseGadget {
775
- name;
776
- description;
777
- parameterSchema;
778
- timeoutMs;
779
- calls = [];
780
- resultValue;
781
- resultFn;
782
- errorToThrow;
783
- delayMs;
784
- shouldTrackCalls;
785
- constructor(config) {
786
- super();
787
- this.name = config.name;
788
- this.description = config.description ?? `Mock gadget: ${config.name}`;
789
- this.parameterSchema = config.schema;
790
- this.resultValue = config.result;
791
- this.resultFn = config.resultFn;
792
- this.delayMs = config.delayMs ?? 0;
793
- this.shouldTrackCalls = config.trackCalls ?? true;
794
- this.timeoutMs = config.timeoutMs;
795
- if (config.error) {
796
- this.errorToThrow = typeof config.error === "string" ? new Error(config.error) : config.error;
797
- }
798
- }
799
- async execute(params) {
800
- if (this.shouldTrackCalls) {
801
- this.calls.push({ params: { ...params }, timestamp: Date.now() });
802
- }
803
- if (this.delayMs > 0) {
804
- await new Promise((resolve) => setTimeout(resolve, this.delayMs));
805
- }
806
- if (this.errorToThrow) {
807
- throw this.errorToThrow;
808
- }
809
- if (this.resultFn) {
810
- return this.resultFn(params);
811
- }
812
- return this.resultValue ?? "mock result";
813
- }
814
- getCalls() {
815
- return [...this.calls];
816
- }
817
- getCallCount() {
818
- return this.calls.length;
819
- }
820
- resetCalls() {
821
- this.calls = [];
822
- }
823
- wasCalledWith(params) {
824
- return this.calls.some(
825
- (call) => Object.entries(params).every(([key, value]) => call.params[key] === value)
826
- );
827
- }
828
- getLastCall() {
829
- return this.calls.length > 0 ? this.calls[this.calls.length - 1] : void 0;
830
- }
831
- };
832
- function createMockGadget(config) {
833
- return new MockGadgetImpl(config);
834
- }
835
- var MockGadgetBuilder = class {
836
- config = { name: "MockGadget" };
837
- /**
838
- * Set the gadget name.
839
- */
840
- withName(name) {
841
- this.config.name = name;
842
- return this;
843
- }
844
- /**
845
- * Set the gadget description.
846
- */
847
- withDescription(description) {
848
- this.config.description = description;
849
- return this;
850
- }
851
- /**
852
- * Set the parameter schema.
853
- */
854
- withSchema(schema) {
855
- this.config.schema = schema;
856
- return this;
857
- }
858
- /**
859
- * Set a static result to return.
860
- */
861
- returns(result) {
862
- this.config.result = result;
863
- this.config.resultFn = void 0;
864
- return this;
865
- }
866
- /**
867
- * Set a dynamic result function.
868
- */
869
- returnsAsync(resultFn) {
870
- this.config.resultFn = resultFn;
871
- this.config.result = void 0;
872
- return this;
873
- }
874
- /**
875
- * Make the gadget throw an error on execution.
876
- */
877
- throws(error) {
878
- this.config.error = error;
879
- return this;
880
- }
881
- /**
882
- * Add execution delay.
883
- */
884
- withDelay(ms) {
885
- this.config.delayMs = ms;
886
- return this;
887
- }
888
- /**
889
- * Set timeout for the gadget.
890
- */
891
- withTimeout(ms) {
892
- this.config.timeoutMs = ms;
893
- return this;
894
- }
895
- /**
896
- * Enable call tracking (enabled by default).
897
- */
898
- trackCalls() {
899
- this.config.trackCalls = true;
900
- return this;
901
- }
902
- /**
903
- * Disable call tracking.
904
- */
905
- noTracking() {
906
- this.config.trackCalls = false;
907
- return this;
908
- }
909
- /**
910
- * Build the mock gadget.
911
- */
912
- build() {
913
- return createMockGadget(this.config);
914
- }
915
- };
916
- function mockGadget() {
917
- return new MockGadgetBuilder();
918
- }
919
-
920
- // src/testing/stream-helpers.ts
921
- function createTestStream(chunks) {
922
- return async function* () {
923
- for (const chunk of chunks) {
924
- yield chunk;
925
- }
926
- }();
927
- }
928
- function createTextStream(text, options) {
929
- return async function* () {
930
- if (options?.delayMs) {
931
- await sleep2(options.delayMs);
932
- }
933
- const chunkSize = options?.chunkSize ?? text.length;
934
- const chunks = [];
935
- for (let i = 0; i < text.length; i += chunkSize) {
936
- chunks.push(text.slice(i, i + chunkSize));
937
- }
938
- for (let i = 0; i < chunks.length; i++) {
939
- const isLast = i === chunks.length - 1;
940
- const chunk = { text: chunks[i] };
941
- if (isLast) {
942
- chunk.finishReason = options?.finishReason ?? "stop";
943
- const inputTokens = Math.ceil(text.length / 4);
944
- const outputTokens = Math.ceil(text.length / 4);
945
- chunk.usage = options?.usage ?? {
946
- inputTokens,
947
- outputTokens,
948
- totalTokens: inputTokens + outputTokens
949
- };
950
- }
951
- yield chunk;
952
- if (options?.chunkDelayMs && !isLast) {
953
- await sleep2(options.chunkDelayMs);
954
- }
955
- }
956
- }();
957
- }
958
- async function collectStream(stream) {
959
- const chunks = [];
960
- for await (const chunk of stream) {
961
- chunks.push(chunk);
962
- }
963
- return chunks;
964
- }
965
- async function collectStreamText(stream) {
966
- let text = "";
967
- for await (const chunk of stream) {
968
- text += chunk.text ?? "";
969
- }
970
- return text;
971
- }
972
- async function getStreamFinalChunk(stream) {
973
- let lastChunk;
974
- for await (const chunk of stream) {
975
- lastChunk = chunk;
976
- }
977
- return lastChunk;
978
- }
979
- function createEmptyStream() {
980
- return async function* () {
981
- }();
982
- }
983
- function createErrorStream(chunksBeforeError, error) {
984
- return async function* () {
985
- for (const chunk of chunksBeforeError) {
986
- yield chunk;
987
- }
988
- throw error;
989
- }();
990
- }
991
- function sleep2(ms) {
992
- return new Promise((resolve) => setTimeout(resolve, ms));
993
- }
994
-
995
- // src/testing/conversation-fixtures.ts
996
- function createConversation(turnCount, options) {
997
- const messages = [];
998
- const userPrefix = options?.userPrefix ?? "User message";
999
- const assistantPrefix = options?.assistantPrefix ?? "Assistant response";
1000
- const contentLength = options?.contentLength ?? 100;
1001
- for (let i = 0; i < turnCount; i++) {
1002
- const padding = " ".repeat(Math.max(0, contentLength - 30));
1003
- messages.push({
1004
- role: "user",
1005
- content: `${userPrefix} ${i + 1}: This is turn ${i + 1} of the conversation.${padding}`
1006
- });
1007
- messages.push({
1008
- role: "assistant",
1009
- content: `${assistantPrefix} ${i + 1}: I acknowledge turn ${i + 1}.${padding}`
1010
- });
1011
- }
1012
- return messages;
1013
- }
1014
- function createConversationWithGadgets(turnCount, gadgetCallsPerTurn = 1, options) {
1015
- const messages = [];
1016
- const gadgetNames = options?.gadgetNames ?? ["search", "calculate", "read"];
1017
- const contentLength = options?.contentLength ?? 50;
1018
- let gadgetIndex = 0;
1019
- for (let turn = 0; turn < turnCount; turn++) {
1020
- messages.push({
1021
- role: "user",
1022
- content: `User request ${turn + 1}${"x".repeat(contentLength)}`
1023
- });
1024
- for (let g = 0; g < gadgetCallsPerTurn; g++) {
1025
- const gadgetName = gadgetNames[gadgetIndex % gadgetNames.length];
1026
- gadgetIndex++;
1027
- messages.push({
1028
- role: "assistant",
1029
- content: `!!!GADGET_START:${gadgetName}
1030
- !!!ARG:query
1031
- test query ${turn}-${g}
1032
- !!!GADGET_END`
1033
- });
1034
- messages.push({
1035
- role: "user",
1036
- content: `Result: Gadget ${gadgetName} returned result for query ${turn}-${g}`
1037
- });
1038
- }
1039
- messages.push({
1040
- role: "assistant",
1041
- content: `Final response for turn ${turn + 1}${"y".repeat(contentLength)}`
1042
- });
1043
- }
1044
- return messages;
1045
- }
1046
- function estimateTokens(messages) {
1047
- return Math.ceil(
1048
- messages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0) / 4
1049
- );
1050
- }
1051
- function createUserMessage(content) {
1052
- return { role: "user", content };
1053
- }
1054
- function createAssistantMessage(content) {
1055
- return { role: "assistant", content };
1056
- }
1057
- function createSystemMessage(content) {
1058
- return { role: "system", content };
1059
- }
1060
- function createMinimalConversation() {
1061
- return [
1062
- { role: "user", content: "Hello" },
1063
- { role: "assistant", content: "Hi there!" }
1064
- ];
1065
- }
1066
- function createLargeConversation(targetTokens, options) {
1067
- const tokensPerTurn = options?.tokensPerTurn ?? 200;
1068
- const turnsNeeded = Math.ceil(targetTokens / tokensPerTurn);
1069
- const charsPerMessage = Math.floor(tokensPerTurn * 4 / 2);
1070
- return createConversation(turnsNeeded, {
1071
- contentLength: charsPerMessage
1072
- });
1073
- }
1074
-
1075
- // src/testing/mock-conversation.ts
1076
- var MockConversationManager = class {
1077
- history;
1078
- baseMessages;
1079
- replacementHistory;
1080
- replaceHistoryCallCount = 0;
1081
- addedMessages = [];
1082
- constructor(history = [], baseMessages = []) {
1083
- this.history = [...history];
1084
- this.baseMessages = [...baseMessages];
1085
- }
1086
- addUserMessage(content) {
1087
- const msg = { role: "user", content };
1088
- this.history.push(msg);
1089
- this.addedMessages.push(msg);
1090
- }
1091
- addAssistantMessage(content) {
1092
- const msg = { role: "assistant", content };
1093
- this.history.push(msg);
1094
- this.addedMessages.push(msg);
1095
- }
1096
- addGadgetCall(gadgetName, parameters, result) {
1097
- const assistantMsg = {
1098
- role: "assistant",
1099
- content: `!!!GADGET_START:${gadgetName}
1100
- ${JSON.stringify(parameters)}
1101
- !!!GADGET_END`
1102
- };
1103
- const resultMsg = {
1104
- role: "user",
1105
- content: `Result: ${result}`
1106
- };
1107
- this.history.push(assistantMsg);
1108
- this.history.push(resultMsg);
1109
- this.addedMessages.push(assistantMsg);
1110
- this.addedMessages.push(resultMsg);
1111
- }
1112
- getMessages() {
1113
- return [...this.baseMessages, ...this.history];
1114
- }
1115
- getHistoryMessages() {
1116
- return [...this.history];
1117
- }
1118
- getBaseMessages() {
1119
- return [...this.baseMessages];
1120
- }
1121
- replaceHistory(newHistory) {
1122
- this.replacementHistory = [...newHistory];
1123
- this.history = [...newHistory];
1124
- this.replaceHistoryCallCount++;
1125
- }
1126
- // ============================================
1127
- // Test Helper Methods
1128
- // ============================================
1129
- /**
1130
- * Check if replaceHistory was called.
1131
- */
1132
- wasReplaceHistoryCalled() {
1133
- return this.replaceHistoryCallCount > 0;
1134
- }
1135
- /**
1136
- * Get the number of times replaceHistory was called.
1137
- */
1138
- getReplaceHistoryCallCount() {
1139
- return this.replaceHistoryCallCount;
1140
- }
1141
- /**
1142
- * Get the most recent history passed to replaceHistory.
1143
- * Returns undefined if replaceHistory was never called.
1144
- */
1145
- getReplacementHistory() {
1146
- return this.replacementHistory;
1147
- }
1148
- /**
1149
- * Get all messages that were added via add* methods.
1150
- */
1151
- getAddedMessages() {
1152
- return [...this.addedMessages];
1153
- }
1154
- /**
1155
- * Reset all tracking state while preserving the conversation.
1156
- */
1157
- resetTracking() {
1158
- this.replacementHistory = void 0;
1159
- this.replaceHistoryCallCount = 0;
1160
- this.addedMessages = [];
1161
- }
1162
- /**
1163
- * Completely reset the mock to initial state.
1164
- * Note: baseMessages cannot be changed after construction.
1165
- */
1166
- reset(history = []) {
1167
- this.history = [...history];
1168
- this.resetTracking();
1169
- }
1170
- /**
1171
- * Set the history directly (for test setup).
1172
- */
1173
- setHistory(messages) {
1174
- this.history = [...messages];
1175
- }
1176
- /**
1177
- * Get the current history length.
1178
- */
1179
- getHistoryLength() {
1180
- return this.history.length;
1181
- }
1182
- /**
1183
- * Get total message count (base + history).
1184
- */
1185
- getTotalMessageCount() {
1186
- return this.baseMessages.length + this.history.length;
1187
- }
1188
- };
1189
- function createMockConversationManager(turnCount, baseMessages = []) {
1190
- const history = [];
1191
- for (let i = 0; i < turnCount; i++) {
1192
- history.push({
1193
- role: "user",
1194
- content: `User message ${i + 1}: This is turn ${i + 1} of the conversation.`
1195
- });
1196
- history.push({
1197
- role: "assistant",
1198
- content: `Assistant response ${i + 1}: I acknowledge turn ${i + 1}.`
1199
- });
1200
- }
1201
- return new MockConversationManager(history, baseMessages);
1202
- }
1203
-
1204
- // src/testing/cli-helpers.ts
1205
- import { PassThrough, Readable, Writable } from "node:stream";
1206
- function createTestEnvironment(options = {}) {
1207
- const stdin = createMockReadable(options.stdin);
1208
- const stdout = new PassThrough();
1209
- const stderr = new PassThrough();
1210
- let exitCode;
1211
- return {
1212
- stdin,
1213
- stdout,
1214
- stderr,
1215
- isTTY: options.isTTY ?? false,
1216
- argv: options.argv ?? ["node", "llmist"],
1217
- env: { ...filterDefinedEnv(process.env), ...options.env },
1218
- get exitCode() {
1219
- return exitCode;
1220
- },
1221
- setExitCode: (code) => {
1222
- exitCode = code;
1223
- }
1224
- };
1225
- }
1226
- function createMockReadable(input) {
1227
- if (!input) {
1228
- const stream2 = new Readable({ read() {
1229
- } });
1230
- stream2.push(null);
1231
- return stream2;
1232
- }
1233
- const content = Array.isArray(input) ? `${input.join("\n")}
1234
- ` : input;
1235
- const stream = new Readable({ read() {
1236
- } });
1237
- stream.push(content);
1238
- stream.push(null);
1239
- return stream;
1240
- }
1241
- function createMockWritable() {
1242
- const chunks = [];
1243
- const stream = new Writable({
1244
- write(chunk, _encoding, callback) {
1245
- chunks.push(Buffer.from(chunk));
1246
- callback();
1247
- }
1248
- });
1249
- stream.getData = () => Buffer.concat(chunks).toString("utf8");
1250
- return stream;
1251
- }
1252
- async function collectOutput(stream, timeout = 5e3) {
1253
- return new Promise((resolve, reject) => {
1254
- const chunks = [];
1255
- const timeoutId = setTimeout(() => {
1256
- resolve(Buffer.concat(chunks).toString("utf8"));
1257
- }, timeout);
1258
- stream.on("data", (chunk) => {
1259
- chunks.push(Buffer.from(chunk));
1260
- });
1261
- stream.on("end", () => {
1262
- clearTimeout(timeoutId);
1263
- resolve(Buffer.concat(chunks).toString("utf8"));
1264
- });
1265
- stream.on("error", (err) => {
1266
- clearTimeout(timeoutId);
1267
- reject(err);
1268
- });
1269
- });
1270
- }
1271
- function getBufferedOutput(stream) {
1272
- const chunks = [];
1273
- for (; ; ) {
1274
- const chunk = stream.read();
1275
- if (chunk === null) break;
1276
- chunks.push(chunk);
1277
- }
1278
- return Buffer.concat(chunks).toString("utf8");
1279
- }
1280
- function createMockPrompt(responses) {
1281
- let index = 0;
1282
- return async (_question) => {
1283
- if (index >= responses.length) {
1284
- throw new Error(`Mock prompt exhausted: no response for question ${index + 1}`);
1285
- }
1286
- return responses[index++];
1287
- };
1288
- }
1289
- var MockPromptRecorder = class {
1290
- responses;
1291
- index = 0;
1292
- questions = [];
1293
- constructor(responses) {
1294
- this.responses = responses;
1295
- }
1296
- /**
1297
- * The prompt function to use in tests.
1298
- */
1299
- prompt = async (question) => {
1300
- this.questions.push(question);
1301
- if (this.index >= this.responses.length) {
1302
- throw new Error(`Mock prompt exhausted after ${this.index} questions`);
1303
- }
1304
- return this.responses[this.index++];
1305
- };
1306
- /**
1307
- * Get all questions that were asked.
1308
- */
1309
- getQuestions() {
1310
- return [...this.questions];
1311
- }
1312
- /**
1313
- * Get the number of questions asked.
1314
- */
1315
- getQuestionCount() {
1316
- return this.questions.length;
1317
- }
1318
- /**
1319
- * Reset the recorder state.
1320
- */
1321
- reset(newResponses) {
1322
- this.index = 0;
1323
- this.questions = [];
1324
- if (newResponses) {
1325
- this.responses = newResponses;
1326
- }
1327
- }
1328
- };
1329
- async function waitFor(condition, timeout = 5e3, interval = 50) {
1330
- const startTime = Date.now();
1331
- while (!condition()) {
1332
- if (Date.now() - startTime > timeout) {
1333
- throw new Error(`waitFor timed out after ${timeout}ms`);
1334
- }
1335
- await sleep3(interval);
1336
- }
1337
- }
1338
- function sleep3(ms) {
1339
- return new Promise((resolve) => setTimeout(resolve, ms));
1340
- }
1341
- function filterDefinedEnv(env) {
1342
- const result = {};
1343
- for (const [key, value] of Object.entries(env)) {
1344
- if (value !== void 0) {
1345
- result[key] = value;
1346
- }
1347
- }
1348
- return result;
1349
- }
1350
-
1351
- export {
1352
- validateAndApplyDefaults,
1353
- validateGadgetParams,
1354
- testGadget,
1355
- testGadgetBatch,
1356
- MockManager,
1357
- getMockManager,
1358
- createMockStream,
1359
- createTextMockStream,
1360
- MockProviderAdapter,
1361
- createMockAdapter,
1362
- MockBuilder,
1363
- mockLLM,
1364
- createMockClient,
1365
- createMockGadget,
1366
- MockGadgetBuilder,
1367
- mockGadget,
1368
- createTestStream,
1369
- createTextStream,
1370
- collectStream,
1371
- collectStreamText,
1372
- getStreamFinalChunk,
1373
- createEmptyStream,
1374
- createErrorStream,
1375
- createConversation,
1376
- createConversationWithGadgets,
1377
- estimateTokens,
1378
- createUserMessage,
1379
- createAssistantMessage,
1380
- createSystemMessage,
1381
- createMinimalConversation,
1382
- createLargeConversation,
1383
- MockConversationManager,
1384
- createMockConversationManager,
1385
- createTestEnvironment,
1386
- createMockReadable,
1387
- createMockWritable,
1388
- collectOutput,
1389
- getBufferedOutput,
1390
- createMockPrompt,
1391
- MockPromptRecorder,
1392
- waitFor
1393
- };
1394
- //# sourceMappingURL=chunk-TFIKR2RK.js.map