@intx/inference-discovery 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,508 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import type { CapabilityIntent } from "./catalog";
6
+ import type { CaptureStep, CapturedResponse, ProviderPlugin } from "./plugin";
7
+ import { runCapture, type FetchLike } from "./runner";
8
+
9
+ const INTENT: CapabilityIntent = { prompt: "hi" };
10
+
11
+ function* singleStepIterator(opts: {
12
+ model: string;
13
+ capability: string;
14
+ intent: CapabilityIntent;
15
+ }): Generator<CaptureStep, void, CapturedResponse> {
16
+ yield {
17
+ kind: "json",
18
+ subdir: null,
19
+ url: `https://example.test/${opts.model}/${opts.capability}`,
20
+ body: { prompt: opts.intent.prompt },
21
+ };
22
+ }
23
+
24
+ function makePlugin(overrides: Partial<ProviderPlugin> = {}): ProviderPlugin {
25
+ return {
26
+ name: "test-provider",
27
+ models: ["test-model"],
28
+ redactRequestHeaders: ["x-api-key"],
29
+ redactResponseHeaders: [],
30
+ buildAuthHeaders: () => ({ "X-Api-Key": "secret-key" }),
31
+ iterateCaptureSteps: singleStepIterator,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ async function makeTempDir(): Promise<string> {
37
+ return await fs.mkdtemp(path.join(os.tmpdir(), "runner-test-"));
38
+ }
39
+
40
+ function bodyToString(body: string | Uint8Array): string {
41
+ return typeof body === "string" ? body : new TextDecoder().decode(body);
42
+ }
43
+
44
+ describe("runCapture", () => {
45
+ let dir: string;
46
+
47
+ beforeEach(async () => {
48
+ dir = await makeTempDir();
49
+ });
50
+
51
+ afterEach(async () => {
52
+ await fs.rm(dir, { recursive: true, force: true });
53
+ });
54
+
55
+ test("captures a JSON response into the expected files", async () => {
56
+ let observedURL = "";
57
+ let observedHeaders: Record<string, string> = {};
58
+ let observedBody: string | Uint8Array = "";
59
+
60
+ const stubFetch: FetchLike = async (url, init) => {
61
+ observedURL = url;
62
+ observedHeaders = init.headers;
63
+ observedBody = init.body;
64
+ return new Response(JSON.stringify({ reply: "hello" }), {
65
+ status: 200,
66
+ headers: { "Content-Type": "application/json" },
67
+ });
68
+ };
69
+
70
+ await runCapture({
71
+ plugin: makePlugin(),
72
+ model: "test-model",
73
+ capability: "plain-text",
74
+ intent: INTENT,
75
+ outDir: dir,
76
+ now: () => new Date("2026-05-22T00:00:00Z"),
77
+ fetch: stubFetch,
78
+ });
79
+
80
+ expect(observedURL).toBe("https://example.test/test-model/plain-text");
81
+ expect(observedHeaders["Content-Type"]).toBe("application/json");
82
+ expect(observedHeaders["X-Api-Key"]).toBe("secret-key");
83
+ expect(JSON.parse(bodyToString(observedBody))).toEqual({ prompt: "hi" });
84
+
85
+ const entries = (await fs.readdir(dir)).sort();
86
+ expect(entries).toEqual([
87
+ "manifest.json",
88
+ "request-headers.json",
89
+ "request.json",
90
+ "response-headers.json",
91
+ "response.json",
92
+ ]);
93
+
94
+ const responseBody = JSON.parse(
95
+ await fs.readFile(path.join(dir, "response.json"), "utf8"),
96
+ );
97
+ expect(responseBody).toEqual({ reply: "hello" });
98
+
99
+ const reqHeaders = JSON.parse(
100
+ await fs.readFile(path.join(dir, "request-headers.json"), "utf8"),
101
+ );
102
+ expect(reqHeaders["X-Api-Key"]).toBe("<REDACTED>");
103
+ expect(reqHeaders["Content-Type"]).toBe("application/json");
104
+
105
+ const manifest = JSON.parse(
106
+ await fs.readFile(path.join(dir, "manifest.json"), "utf8"),
107
+ );
108
+ expect(manifest).toEqual({
109
+ provider: "test-provider",
110
+ model: "test-model",
111
+ capability: "plain-text",
112
+ capturedAt: "2026-05-22T00:00:00.000Z",
113
+ schemaVersion: "1",
114
+ });
115
+ });
116
+
117
+ test("captures an SSE response into response.sse only", async () => {
118
+ const sseBody = 'data: {"chunk":1}\n\ndata: {"chunk":2}\n\n';
119
+ const stubFetch: FetchLike = async () =>
120
+ new Response(sseBody, {
121
+ status: 200,
122
+ headers: { "Content-Type": "text/event-stream" },
123
+ });
124
+
125
+ await runCapture({
126
+ plugin: makePlugin(),
127
+ model: "test-model",
128
+ capability: "plain-text-streaming",
129
+ intent: INTENT,
130
+ outDir: dir,
131
+ now: () => new Date("2026-05-22T00:00:00Z"),
132
+ fetch: stubFetch,
133
+ });
134
+
135
+ const entries = (await fs.readdir(dir)).sort();
136
+ expect(entries).toContain("response.sse");
137
+ expect(entries).not.toContain("response.json");
138
+
139
+ const written = await fs.readFile(path.join(dir, "response.sse"), "utf8");
140
+ expect(written).toBe(sseBody);
141
+ });
142
+
143
+ test("throws on unsupported response content-type", async () => {
144
+ const stubFetch: FetchLike = async () =>
145
+ new Response("oops", {
146
+ status: 200,
147
+ headers: { "Content-Type": "text/plain" },
148
+ });
149
+
150
+ await expect(
151
+ runCapture({
152
+ plugin: makePlugin(),
153
+ model: "test-model",
154
+ capability: "plain-text",
155
+ intent: INTENT,
156
+ outDir: dir,
157
+ fetch: stubFetch,
158
+ }),
159
+ ).rejects.toThrow(/text\/plain/);
160
+ });
161
+
162
+ test("invokes extractReasoningTrace for reasoning-content captures and writes the trace", async () => {
163
+ let invoked = false;
164
+ let payload: unknown = null;
165
+ const plugin = makePlugin({
166
+ extractReasoningTrace: (parsed) => {
167
+ invoked = true;
168
+ payload = parsed;
169
+ return { fieldPath: "x", text: "thoughts" };
170
+ },
171
+ });
172
+ const stubFetch: FetchLike = async () =>
173
+ new Response(JSON.stringify({ reasoning: "step", final: "answer" }), {
174
+ status: 200,
175
+ headers: { "Content-Type": "application/json" },
176
+ });
177
+
178
+ await runCapture({
179
+ plugin,
180
+ model: "test-model",
181
+ capability: "reasoning-content",
182
+ intent: INTENT,
183
+ outDir: dir,
184
+ fetch: stubFetch,
185
+ });
186
+
187
+ expect(invoked).toBe(true);
188
+ expect(payload).toEqual({ reasoning: "step", final: "answer" });
189
+
190
+ const trace = JSON.parse(
191
+ await fs.readFile(path.join(dir, "reasoning-trace.json"), "utf8"),
192
+ );
193
+ expect(trace).toEqual({ fieldPath: "x", text: "thoughts" });
194
+ });
195
+
196
+ test("invokes extractReasoningTrace for redacted-thinking captures and writes the trace", async () => {
197
+ let invoked = false;
198
+ const plugin = makePlugin({
199
+ extractReasoningTrace: () => {
200
+ invoked = true;
201
+ return { fieldPath: "content[0].data", text: "<redacted>" };
202
+ },
203
+ });
204
+ const stubFetch: FetchLike = async () =>
205
+ new Response(JSON.stringify({ ok: true }), {
206
+ status: 200,
207
+ headers: { "Content-Type": "application/json" },
208
+ });
209
+
210
+ await runCapture({
211
+ plugin,
212
+ model: "test-model",
213
+ capability: "redacted-thinking",
214
+ intent: INTENT,
215
+ outDir: dir,
216
+ fetch: stubFetch,
217
+ });
218
+
219
+ expect(invoked).toBe(true);
220
+ const trace = JSON.parse(
221
+ await fs.readFile(path.join(dir, "reasoning-trace.json"), "utf8"),
222
+ );
223
+ expect(trace).toEqual({ fieldPath: "content[0].data", text: "<redacted>" });
224
+ });
225
+
226
+ test("does not write reasoning-trace.json when extractReasoningTrace returns null", async () => {
227
+ const plugin = makePlugin({
228
+ extractReasoningTrace: () => null,
229
+ });
230
+ const stubFetch: FetchLike = async () =>
231
+ new Response(JSON.stringify({}), {
232
+ status: 200,
233
+ headers: { "Content-Type": "application/json" },
234
+ });
235
+
236
+ await runCapture({
237
+ plugin,
238
+ model: "test-model",
239
+ capability: "reasoning-content",
240
+ intent: INTENT,
241
+ outDir: dir,
242
+ fetch: stubFetch,
243
+ });
244
+
245
+ const entries = await fs.readdir(dir);
246
+ expect(entries).not.toContain("reasoning-trace.json");
247
+ });
248
+
249
+ test("does not invoke extractReasoningTrace for non-reasoning captures", async () => {
250
+ let invoked = false;
251
+ const plugin = makePlugin({
252
+ extractReasoningTrace: () => {
253
+ invoked = true;
254
+ return null;
255
+ },
256
+ });
257
+ const stubFetch: FetchLike = async () =>
258
+ new Response(JSON.stringify({}), {
259
+ status: 200,
260
+ headers: { "Content-Type": "application/json" },
261
+ });
262
+
263
+ await runCapture({
264
+ plugin,
265
+ model: "test-model",
266
+ capability: "plain-text",
267
+ intent: INTENT,
268
+ outDir: dir,
269
+ fetch: stubFetch,
270
+ });
271
+
272
+ expect(invoked).toBe(false);
273
+ });
274
+
275
+ test("walks all steps of a multi-step generator and writes them into subdirs", async () => {
276
+ let fetchCalls = 0;
277
+ const observedURLs: string[] = [];
278
+ const observedBodies: unknown[] = [];
279
+
280
+ function* twoStep(opts: {
281
+ model: string;
282
+ capability: string;
283
+ intent: CapabilityIntent;
284
+ }): Generator<CaptureStep, void, CapturedResponse> {
285
+ const first = yield {
286
+ kind: "json",
287
+ subdir: "turn-1",
288
+ url: `https://example.test/${opts.model}/${opts.capability}/turn-1`,
289
+ body: { prompt: opts.intent.prompt },
290
+ };
291
+ yield {
292
+ kind: "json",
293
+ subdir: "turn-2",
294
+ url: `https://example.test/${opts.model}/${opts.capability}/turn-2`,
295
+ body: { prior: first.parsed, prompt: "follow-up" },
296
+ };
297
+ }
298
+
299
+ const plugin = makePlugin({ iterateCaptureSteps: twoStep });
300
+
301
+ const stubFetch: FetchLike = async (url, init) => {
302
+ fetchCalls += 1;
303
+ observedURLs.push(url);
304
+ observedBodies.push(JSON.parse(bodyToString(init.body)));
305
+ return new Response(JSON.stringify({ step: fetchCalls }), {
306
+ status: 200,
307
+ headers: { "Content-Type": "application/json" },
308
+ });
309
+ };
310
+
311
+ await runCapture({
312
+ plugin,
313
+ model: "test-model",
314
+ capability: "function-calling-multi-turn",
315
+ intent: INTENT,
316
+ outDir: dir,
317
+ fetch: stubFetch,
318
+ });
319
+
320
+ expect(fetchCalls).toBe(2);
321
+ expect(observedURLs).toEqual([
322
+ "https://example.test/test-model/function-calling-multi-turn/turn-1",
323
+ "https://example.test/test-model/function-calling-multi-turn/turn-2",
324
+ ]);
325
+ expect(observedBodies[1]).toEqual({
326
+ prior: { step: 1 },
327
+ prompt: "follow-up",
328
+ });
329
+
330
+ const turn1Entries = (await fs.readdir(path.join(dir, "turn-1"))).sort();
331
+ expect(turn1Entries).toEqual([
332
+ "request-headers.json",
333
+ "request.json",
334
+ "response-headers.json",
335
+ "response.json",
336
+ ]);
337
+ const turn2Body = JSON.parse(
338
+ await fs.readFile(path.join(dir, "turn-2", "response.json"), "utf8"),
339
+ );
340
+ expect(turn2Body).toEqual({ step: 2 });
341
+
342
+ const rootEntries = (await fs.readdir(dir)).sort();
343
+ expect(rootEntries).toContain("manifest.json");
344
+ const manifest = JSON.parse(
345
+ await fs.readFile(path.join(dir, "manifest.json"), "utf8"),
346
+ );
347
+ expect(manifest).toMatchObject({
348
+ provider: "test-provider",
349
+ model: "test-model",
350
+ capability: "function-calling-multi-turn",
351
+ schemaVersion: "1",
352
+ });
353
+ });
354
+
355
+ test("captures a raw-bytes step into request.bin with the supplied content-type", async () => {
356
+ const payload = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31]);
357
+ let observedHeaders: Record<string, string> = {};
358
+ let observedBody: string | Uint8Array = "";
359
+
360
+ function* rawIterator(): Generator<CaptureStep, void, CapturedResponse> {
361
+ yield {
362
+ kind: "raw",
363
+ subdir: "upload",
364
+ url: "https://example.test/upload",
365
+ method: "POST",
366
+ contentType: "application/pdf",
367
+ headers: { "X-Upload-Protocol": "raw" },
368
+ body: payload,
369
+ };
370
+ }
371
+
372
+ const plugin = makePlugin({ iterateCaptureSteps: rawIterator });
373
+ const stubFetch: FetchLike = async (_url, init) => {
374
+ observedHeaders = init.headers;
375
+ observedBody = init.body;
376
+ return new Response(JSON.stringify({ fileId: "abc" }), {
377
+ status: 200,
378
+ headers: { "Content-Type": "application/json" },
379
+ });
380
+ };
381
+
382
+ await runCapture({
383
+ plugin,
384
+ model: "test-model",
385
+ capability: "files-api-reference",
386
+ intent: INTENT,
387
+ outDir: dir,
388
+ fetch: stubFetch,
389
+ });
390
+
391
+ expect(observedHeaders["Content-Type"]).toBe("application/pdf");
392
+ expect(observedHeaders["X-Upload-Protocol"]).toBe("raw");
393
+ expect(observedHeaders["X-Api-Key"]).toBe("secret-key");
394
+ if (typeof observedBody === "string") {
395
+ throw new Error("expected raw step to send Uint8Array body");
396
+ }
397
+ expect(observedBody).toBeInstanceOf(Uint8Array);
398
+ expect(Array.from(observedBody)).toEqual(Array.from(payload));
399
+
400
+ const uploadEntries = (await fs.readdir(path.join(dir, "upload"))).sort();
401
+ expect(uploadEntries).toEqual([
402
+ "request-headers.json",
403
+ "request.bin",
404
+ "response-headers.json",
405
+ "response.json",
406
+ ]);
407
+ const writtenBytes = await fs.readFile(
408
+ path.join(dir, "upload", "request.bin"),
409
+ );
410
+ expect(Array.from(writtenBytes)).toEqual(Array.from(payload));
411
+ });
412
+
413
+ test("rejects step.headers that collide with plug-in auth headers", async () => {
414
+ function* collidingIterator(): Generator<
415
+ CaptureStep,
416
+ void,
417
+ CapturedResponse
418
+ > {
419
+ yield {
420
+ kind: "json",
421
+ subdir: null,
422
+ url: "https://example.test/collide",
423
+ headers: { "x-api-key": "step-override" },
424
+ body: {},
425
+ };
426
+ }
427
+ const plugin = makePlugin({ iterateCaptureSteps: collidingIterator });
428
+ const stubFetch: FetchLike = async () =>
429
+ new Response("{}", {
430
+ status: 200,
431
+ headers: { "Content-Type": "application/json" },
432
+ });
433
+
434
+ await expect(
435
+ runCapture({
436
+ plugin,
437
+ model: "test-model",
438
+ capability: "plain-text",
439
+ intent: INTENT,
440
+ outDir: dir,
441
+ fetch: stubFetch,
442
+ }),
443
+ ).rejects.toThrow(/override plug-in auth header/);
444
+ });
445
+
446
+ test("step.headers can override the default content-type without auth collision", async () => {
447
+ let observedHeaders: Record<string, string> = {};
448
+
449
+ function* overrideIterator(): Generator<
450
+ CaptureStep,
451
+ void,
452
+ CapturedResponse
453
+ > {
454
+ yield {
455
+ kind: "json",
456
+ subdir: null,
457
+ url: "https://example.test/override",
458
+ headers: { "Content-Type": "application/x-overridden+json" },
459
+ body: { x: 1 },
460
+ };
461
+ }
462
+ const plugin = makePlugin({ iterateCaptureSteps: overrideIterator });
463
+ const stubFetch: FetchLike = async (_url, init) => {
464
+ observedHeaders = init.headers;
465
+ return new Response("{}", {
466
+ status: 200,
467
+ headers: { "Content-Type": "application/json" },
468
+ });
469
+ };
470
+
471
+ await runCapture({
472
+ plugin,
473
+ model: "test-model",
474
+ capability: "plain-text",
475
+ intent: INTENT,
476
+ outDir: dir,
477
+ fetch: stubFetch,
478
+ });
479
+
480
+ expect(observedHeaders["Content-Type"]).toBe(
481
+ "application/x-overridden+json",
482
+ );
483
+ expect(observedHeaders["X-Api-Key"]).toBe("secret-key");
484
+ });
485
+
486
+ test("throws when the iterator yields no steps", async () => {
487
+ function* empty(): Generator<CaptureStep, void, CapturedResponse> {
488
+ // intentionally yields nothing
489
+ }
490
+ const plugin = makePlugin({ iterateCaptureSteps: empty });
491
+ const stubFetch: FetchLike = async () =>
492
+ new Response("{}", {
493
+ status: 200,
494
+ headers: { "Content-Type": "application/json" },
495
+ });
496
+
497
+ await expect(
498
+ runCapture({
499
+ plugin,
500
+ model: "test-model",
501
+ capability: "plain-text",
502
+ intent: INTENT,
503
+ outDir: dir,
504
+ fetch: stubFetch,
505
+ }),
506
+ ).rejects.toThrow(/no capture steps/);
507
+ });
508
+ });