@intx/inference-discovery-anthropic 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,681 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ INTENTS,
4
+ type Capability,
5
+ type CapabilityIntent,
6
+ } from "@intx/inference-discovery/catalog";
7
+ import type { CaptureStep, CapturedResponse } from "@intx/inference-discovery";
8
+ import { ANTHROPIC_VERSION } from "./auth";
9
+ import {
10
+ buildFilesURL,
11
+ buildMessagesURL,
12
+ isStreamingCapability,
13
+ } from "./endpoint";
14
+ import {
15
+ buildFilesApiGenerateBody,
16
+ buildFunctionCallingTurn2Body,
17
+ buildRedactedThinkingTurn2Body,
18
+ buildRequestBody,
19
+ isSupportedCapability,
20
+ } from "./request-body";
21
+ import { extractReasoningTrace } from "./reasoning";
22
+ import { createAnthropicPlugin, iterateCaptureSteps } from "./index";
23
+ import type {
24
+ AnthropicContentBlock,
25
+ AnthropicMessage,
26
+ AnthropicRequestBody,
27
+ } from "./request-body";
28
+
29
+ function isAnthropicBody(value: unknown): value is AnthropicRequestBody {
30
+ if (typeof value !== "object" || value === null) return false;
31
+ if (!("model" in value) || typeof value.model !== "string") return false;
32
+ if (!("max_tokens" in value) || typeof value.max_tokens !== "number") {
33
+ return false;
34
+ }
35
+ if (!("messages" in value) || !Array.isArray(value.messages)) return false;
36
+ return true;
37
+ }
38
+
39
+ function expectAnthropicBody(value: unknown): AnthropicRequestBody {
40
+ if (!isAnthropicBody(value)) {
41
+ throw new Error(
42
+ "expected AnthropicRequestBody {model: string, max_tokens: number, messages: array}",
43
+ );
44
+ }
45
+ return value;
46
+ }
47
+
48
+ function expectArrayContent(
49
+ message: AnthropicMessage,
50
+ ): AnthropicContentBlock[] {
51
+ if (typeof message.content === "string") {
52
+ throw new Error("expected an array-content message");
53
+ }
54
+ return message.content;
55
+ }
56
+
57
+ const TEST_API_KEY = "test-anthropic-key";
58
+ const SONNET = "claude-sonnet-4-5-20250929";
59
+
60
+ function collectSteps(opts: {
61
+ model: string;
62
+ capability: Capability;
63
+ responses: readonly CapturedResponse[];
64
+ }): CaptureStep[] {
65
+ const intent = INTENTS[opts.capability];
66
+ const iter = iterateCaptureSteps({
67
+ model: opts.model,
68
+ capability: opts.capability,
69
+ intent,
70
+ });
71
+ const steps: CaptureStep[] = [];
72
+ let i = 0;
73
+ let next = iter.next();
74
+ while (!next.done) {
75
+ steps.push(next.value);
76
+ const response = opts.responses[i];
77
+ i += 1;
78
+ if (response === undefined) break;
79
+ next = iter.next(response);
80
+ }
81
+ return steps;
82
+ }
83
+
84
+ describe("createAnthropicPlugin", () => {
85
+ test("declares provider name, all three models, and redaction lists", () => {
86
+ const plugin = createAnthropicPlugin({ apiKey: TEST_API_KEY });
87
+ expect(plugin.name).toBe("anthropic");
88
+ expect(plugin.models).toEqual([
89
+ "claude-sonnet-4-5-20250929",
90
+ "claude-opus-4-1-20250805",
91
+ "claude-haiku-4-5-20251001",
92
+ ]);
93
+ expect(plugin.redactRequestHeaders).toEqual(["x-api-key"]);
94
+ expect(plugin.redactResponseHeaders).toEqual([]);
95
+ });
96
+
97
+ test("buildAuthHeaders returns x-api-key and pinned anthropic-version", () => {
98
+ const plugin = createAnthropicPlugin({ apiKey: TEST_API_KEY });
99
+ expect(plugin.buildAuthHeaders()).toEqual({
100
+ "x-api-key": TEST_API_KEY,
101
+ "anthropic-version": ANTHROPIC_VERSION,
102
+ });
103
+ });
104
+
105
+ test("buildAuthHeaders never carries an anthropic-beta flag", () => {
106
+ const plugin = createAnthropicPlugin({ apiKey: TEST_API_KEY });
107
+ const headers = plugin.buildAuthHeaders();
108
+ expect(headers["anthropic-beta"]).toBeUndefined();
109
+ });
110
+
111
+ test("buildAuthHeaders rejects an empty apiKey", () => {
112
+ const plugin = createAnthropicPlugin({ apiKey: "" });
113
+ expect(() => plugin.buildAuthHeaders()).toThrow(/apiKey/);
114
+ });
115
+ });
116
+
117
+ describe("endpoint URL helpers", () => {
118
+ test("messages URL is the v1/messages endpoint regardless of streaming", () => {
119
+ expect(buildMessagesURL()).toBe("https://api.anthropic.com/v1/messages");
120
+ });
121
+
122
+ test("files URL is the v1/files endpoint", () => {
123
+ expect(buildFilesURL()).toBe("https://api.anthropic.com/v1/files");
124
+ });
125
+
126
+ test("isStreamingCapability matches the -streaming suffix", () => {
127
+ expect(isStreamingCapability("plain-text")).toBe(false);
128
+ expect(isStreamingCapability("plain-text-streaming")).toBe(true);
129
+ expect(isStreamingCapability("redacted-thinking-streaming")).toBe(true);
130
+ });
131
+ });
132
+
133
+ describe("buildRequestBody — wire-shape spot checks", () => {
134
+ test("plain-text produces a single user message with max_tokens, no stream flag", () => {
135
+ const body = buildRequestBody({
136
+ model: SONNET,
137
+ capability: "plain-text",
138
+ intent: INTENTS["plain-text"],
139
+ });
140
+ expect(body).toEqual({
141
+ model: SONNET,
142
+ max_tokens: 512,
143
+ messages: [{ role: "user", content: INTENTS["plain-text"].prompt }],
144
+ });
145
+ });
146
+
147
+ test("plain-text-streaming sets stream: true", () => {
148
+ const body = buildRequestBody({
149
+ model: SONNET,
150
+ capability: "plain-text-streaming",
151
+ intent: INTENTS["plain-text-streaming"],
152
+ });
153
+ expect(body.stream).toBe(true);
154
+ });
155
+
156
+ test("function-calling carries the tool decl as input_schema", () => {
157
+ const body = buildRequestBody({
158
+ model: SONNET,
159
+ capability: "function-calling",
160
+ intent: INTENTS["function-calling"],
161
+ });
162
+ expect(body.tools).toBeDefined();
163
+ const tools = body.tools;
164
+ if (tools === undefined) throw new Error("tools missing");
165
+ const [tool] = tools;
166
+ if (tool === undefined || "type" in tool) {
167
+ throw new Error("expected function tool decl, got server-side tool");
168
+ }
169
+ expect(tool.name).toBe("get_weather");
170
+ expect(tool.input_schema.type).toBe("object");
171
+ });
172
+
173
+ test("function-calling-with-thinking attaches thinking config + raised max_tokens", () => {
174
+ const body = buildRequestBody({
175
+ model: SONNET,
176
+ capability: "function-calling-with-thinking",
177
+ intent: INTENTS["function-calling-with-thinking"],
178
+ });
179
+ expect(body.thinking).toEqual({ type: "enabled", budget_tokens: 1024 });
180
+ expect(body.max_tokens).toBeGreaterThan(1024);
181
+ });
182
+
183
+ test("reasoning-content enables thinking with budget_tokens=1024", () => {
184
+ const body = buildRequestBody({
185
+ model: SONNET,
186
+ capability: "reasoning-content",
187
+ intent: INTENTS["reasoning-content"],
188
+ });
189
+ expect(body.thinking).toEqual({ type: "enabled", budget_tokens: 1024 });
190
+ });
191
+
192
+ test("vision-input embeds a base64 image source", () => {
193
+ const body = buildRequestBody({
194
+ model: SONNET,
195
+ capability: "vision-input",
196
+ intent: INTENTS["vision-input"],
197
+ });
198
+ const first = body.messages[0];
199
+ if (first === undefined || typeof first.content === "string") {
200
+ throw new Error("expected an array-content user turn");
201
+ }
202
+ const image = first.content.find((b) => b.type === "image");
203
+ if (image === undefined || image.type !== "image") {
204
+ throw new Error("vision-input must carry an image content block");
205
+ }
206
+ expect(image.source.type).toBe("base64");
207
+ expect(image.source.media_type).toBe("image/jpeg");
208
+ expect(image.source.data.length).toBeGreaterThan(0);
209
+ });
210
+
211
+ test("document-input embeds a base64 application/pdf source", () => {
212
+ const body = buildRequestBody({
213
+ model: SONNET,
214
+ capability: "document-input",
215
+ intent: INTENTS["document-input"],
216
+ });
217
+ const first = body.messages[0];
218
+ if (first === undefined || typeof first.content === "string") {
219
+ throw new Error("expected an array-content user turn");
220
+ }
221
+ const doc = first.content.find((b) => b.type === "document");
222
+ if (doc === undefined || doc.type !== "document") {
223
+ throw new Error("document-input must carry a document content block");
224
+ }
225
+ if (doc.source.type !== "base64") {
226
+ throw new Error("document-input must use base64 source on inline path");
227
+ }
228
+ expect(doc.source.media_type).toBe("application/pdf");
229
+ expect(doc.source.data.length).toBeGreaterThan(0);
230
+ });
231
+
232
+ test("code-execution declares the server-side code_execution tool", () => {
233
+ const body = buildRequestBody({
234
+ model: SONNET,
235
+ capability: "code-execution",
236
+ intent: INTENTS["code-execution"],
237
+ });
238
+ expect(body.tools).toEqual([
239
+ { type: "code_execution_20250522", name: "code_execution" },
240
+ ]);
241
+ });
242
+
243
+ test("grounding declares the server-side web_search tool", () => {
244
+ const body = buildRequestBody({
245
+ model: SONNET,
246
+ capability: "grounding",
247
+ intent: INTENTS["grounding"],
248
+ });
249
+ expect(body.tools).toEqual([
250
+ { type: "web_search_20250305", name: "web_search" },
251
+ ]);
252
+ });
253
+
254
+ test("throws for files-api capabilities (use iterateCaptureSteps for multipart upload)", () => {
255
+ const multipart: Capability[] = [
256
+ "files-api-reference",
257
+ "files-api-reference-streaming",
258
+ ];
259
+ for (const capability of multipart) {
260
+ expect(() =>
261
+ buildRequestBody({
262
+ model: SONNET,
263
+ capability,
264
+ intent: INTENTS[capability],
265
+ }),
266
+ ).toThrow(/multipart/);
267
+ }
268
+ });
269
+
270
+ test("multi-turn capabilities return the turn-1 body (used by iterateCaptureSteps)", () => {
271
+ const turn1 = buildRequestBody({
272
+ model: SONNET,
273
+ capability: "function-calling-multi-turn",
274
+ intent: INTENTS["function-calling-multi-turn"],
275
+ });
276
+ expect(turn1.tools).toBeDefined();
277
+ expect(turn1.thinking).toBeUndefined();
278
+ expect(turn1.stream).toBeUndefined();
279
+ });
280
+
281
+ test("redacted-thinking returns a turn-1 body with thinking enabled", () => {
282
+ const turn1 = buildRequestBody({
283
+ model: SONNET,
284
+ capability: "redacted-thinking",
285
+ intent: INTENTS["redacted-thinking"],
286
+ });
287
+ expect(turn1.thinking).toEqual({ type: "enabled", budget_tokens: 1024 });
288
+ expect(turn1.messages[0]?.content).toMatch(/ANTHROPIC_MAGIC_STRING/);
289
+ });
290
+
291
+ test("throws for capabilities Anthropic does not expose", () => {
292
+ const unsupported: Capability[] = [
293
+ "audio-input",
294
+ "audio-input-streaming",
295
+ "video-input",
296
+ "video-input-streaming",
297
+ "image-output",
298
+ "image-output-streaming",
299
+ "safety-classification",
300
+ "safety-classification-streaming",
301
+ ];
302
+ for (const capability of unsupported) {
303
+ expect(() =>
304
+ buildRequestBody({
305
+ model: SONNET,
306
+ capability,
307
+ intent: INTENTS[capability],
308
+ }),
309
+ ).toThrow(/not supported/);
310
+ }
311
+ });
312
+ });
313
+
314
+ describe("buildFunctionCallingTurn2Body", () => {
315
+ test("echoes assistant content verbatim and appends a tool_result user turn", () => {
316
+ const intent = INTENTS["function-calling-multi-turn"];
317
+ const turn1 = buildRequestBody({
318
+ model: SONNET,
319
+ capability: "function-calling-multi-turn",
320
+ intent,
321
+ });
322
+ const assistantBlocks: AnthropicContentBlock[] = [
323
+ { type: "text", text: "Calling tool" },
324
+ {
325
+ type: "tool_use",
326
+ id: "tool_use_1",
327
+ name: "get_weather",
328
+ input: { location: "Boston, MA" },
329
+ },
330
+ ];
331
+ const turn1Response = { content: assistantBlocks };
332
+ const turn2 = buildFunctionCallingTurn2Body({
333
+ model: SONNET,
334
+ capability: "function-calling-multi-turn",
335
+ intent,
336
+ turn1Body: turn1,
337
+ turn1Response,
338
+ });
339
+ expect(turn2.messages.length).toBe(turn1.messages.length + 2);
340
+ const assistant = turn2.messages[turn1.messages.length];
341
+ expect(assistant?.role).toBe("assistant");
342
+ expect(assistant?.content).toEqual(turn1Response.content);
343
+ const userTurn = turn2.messages[turn1.messages.length + 1];
344
+ expect(userTurn?.role).toBe("user");
345
+ if (userTurn === undefined || typeof userTurn.content === "string") {
346
+ throw new Error("expected an array-content user follow-up");
347
+ }
348
+ const toolResult = userTurn.content[0];
349
+ expect(toolResult).toEqual({
350
+ type: "tool_result",
351
+ tool_use_id: "tool_use_1",
352
+ content:
353
+ '{"location":"Boston, MA","temperatureF":68,"conditions":"clear"}',
354
+ });
355
+ });
356
+
357
+ test("throws when turn-1 lacks a tool_use block", () => {
358
+ const intent = INTENTS["function-calling-multi-turn"];
359
+ const turn1 = buildRequestBody({
360
+ model: SONNET,
361
+ capability: "function-calling-multi-turn",
362
+ intent,
363
+ });
364
+ expect(() =>
365
+ buildFunctionCallingTurn2Body({
366
+ model: SONNET,
367
+ capability: "function-calling-multi-turn",
368
+ intent,
369
+ turn1Body: turn1,
370
+ turn1Response: { content: [{ type: "text", text: "no tool" }] },
371
+ }),
372
+ ).toThrow(/tool_use/);
373
+ });
374
+ });
375
+
376
+ describe("buildRedactedThinkingTurn2Body", () => {
377
+ test("echoes redacted_thinking blocks verbatim and appends a user follow-up", () => {
378
+ const intent: CapabilityIntent = INTENTS["redacted-thinking"];
379
+ const turn1: ReturnType<typeof buildFunctionCallingTurn2Body> = {
380
+ model: SONNET,
381
+ max_tokens: 2048,
382
+ messages: [{ role: "user", content: intent.prompt }],
383
+ thinking: { type: "enabled", budget_tokens: 1024 },
384
+ };
385
+ const assistantBlocks: AnthropicContentBlock[] = [
386
+ { type: "redacted_thinking", data: "encrypted-bytes" },
387
+ { type: "text", text: "Continuing." },
388
+ ];
389
+ const turn1Response = { content: assistantBlocks };
390
+ const turn2 = buildRedactedThinkingTurn2Body({
391
+ model: SONNET,
392
+ intent,
393
+ turn1Body: turn1,
394
+ turn1Response,
395
+ });
396
+ expect(turn2.thinking).toEqual({ type: "enabled", budget_tokens: 1024 });
397
+ expect(turn2.messages.length).toBe(3);
398
+ const assistant = turn2.messages[1];
399
+ expect(assistant?.role).toBe("assistant");
400
+ expect(assistant?.content).toEqual(turn1Response.content);
401
+ const followUp = turn2.messages[2];
402
+ expect(followUp).toEqual({
403
+ role: "user",
404
+ content: "Briefly summarize what you just said in one sentence.",
405
+ });
406
+ });
407
+ });
408
+
409
+ describe("buildFilesApiGenerateBody", () => {
410
+ test("references the uploaded file by file_id and uses the intent prompt", () => {
411
+ const body = buildFilesApiGenerateBody({
412
+ model: SONNET,
413
+ fileId: "file_abc",
414
+ intent: INTENTS["files-api-reference"],
415
+ stream: false,
416
+ });
417
+ const first = body.messages[0];
418
+ if (first === undefined || typeof first.content === "string") {
419
+ throw new Error("expected an array-content message");
420
+ }
421
+ expect(first.content[0]).toEqual({
422
+ type: "document",
423
+ source: { type: "file", file_id: "file_abc" },
424
+ });
425
+ expect(first.content[1]).toEqual({
426
+ type: "text",
427
+ text: INTENTS["files-api-reference"].prompt,
428
+ });
429
+ expect(body.stream).toBeUndefined();
430
+ });
431
+
432
+ test("sets stream when the streaming variant is requested", () => {
433
+ const body = buildFilesApiGenerateBody({
434
+ model: SONNET,
435
+ fileId: "file_abc",
436
+ intent: INTENTS["files-api-reference-streaming"],
437
+ stream: true,
438
+ });
439
+ expect(body.stream).toBe(true);
440
+ });
441
+ });
442
+
443
+ describe("iterateCaptureSteps", () => {
444
+ test("single-step capability yields one JSON step with no subdir", () => {
445
+ const steps = collectSteps({
446
+ model: SONNET,
447
+ capability: "plain-text",
448
+ responses: [],
449
+ });
450
+ expect(steps.length).toBe(1);
451
+ const [only] = steps;
452
+ if (only === undefined) throw new Error("missing step");
453
+ expect(only.kind).toBe("json");
454
+ expect(only.subdir).toBeNull();
455
+ expect(only.url).toBe("https://api.anthropic.com/v1/messages");
456
+ });
457
+
458
+ test("multi-turn function-calling yields turn-1 then turn-2 JSON steps", () => {
459
+ const turn1Response: CapturedResponse = {
460
+ status: 200,
461
+ headers: {},
462
+ parsed: {
463
+ content: [
464
+ {
465
+ type: "tool_use",
466
+ id: "tool_use_1",
467
+ name: "get_weather",
468
+ input: { location: "Boston, MA" },
469
+ },
470
+ ],
471
+ },
472
+ bytes: null,
473
+ };
474
+ const steps = collectSteps({
475
+ model: SONNET,
476
+ capability: "function-calling-multi-turn",
477
+ responses: [turn1Response],
478
+ });
479
+ expect(steps.length).toBe(2);
480
+ expect(steps[0]?.subdir).toBe("turn-1");
481
+ expect(steps[1]?.subdir).toBe("turn-2");
482
+ for (const step of steps) {
483
+ expect(step.kind).toBe("json");
484
+ expect(step.url).toBe("https://api.anthropic.com/v1/messages");
485
+ }
486
+ });
487
+
488
+ test("redacted-thinking yields turn-1 then turn-2 JSON steps", () => {
489
+ const turn1Response: CapturedResponse = {
490
+ status: 200,
491
+ headers: {},
492
+ parsed: {
493
+ content: [
494
+ { type: "redacted_thinking", data: "opaque-bytes" },
495
+ { type: "text", text: "Done." },
496
+ ],
497
+ },
498
+ bytes: null,
499
+ };
500
+ const steps = collectSteps({
501
+ model: SONNET,
502
+ capability: "redacted-thinking",
503
+ responses: [turn1Response],
504
+ });
505
+ expect(steps.length).toBe(2);
506
+ expect(steps[0]?.subdir).toBe("turn-1");
507
+ expect(steps[1]?.subdir).toBe("turn-2");
508
+ });
509
+
510
+ test("files-api yields a raw upload step then a JSON generate step", () => {
511
+ const uploadResponse: CapturedResponse = {
512
+ status: 200,
513
+ headers: {},
514
+ parsed: { id: "file_abc", filename: "sample.pdf" },
515
+ bytes: null,
516
+ };
517
+ const steps = collectSteps({
518
+ model: SONNET,
519
+ capability: "files-api-reference",
520
+ responses: [uploadResponse],
521
+ });
522
+ expect(steps.length).toBe(2);
523
+ const [upload, generate] = steps;
524
+ if (upload === undefined || generate === undefined) {
525
+ throw new Error("missing steps");
526
+ }
527
+ expect(upload.kind).toBe("raw");
528
+ expect(upload.subdir).toBe("upload");
529
+ expect(upload.url).toBe("https://api.anthropic.com/v1/files");
530
+ if (upload.kind !== "raw") throw new Error("unreachable");
531
+ expect(upload.contentType).toMatch(/^multipart\/form-data; boundary=/);
532
+ expect(upload.headers?.["anthropic-beta"]).toBe("files-api-2025-04-14");
533
+ expect(upload.body.byteLength).toBeGreaterThan(0);
534
+
535
+ expect(generate.kind).toBe("json");
536
+ expect(generate.subdir).toBe("generate");
537
+ expect(generate.url).toBe("https://api.anthropic.com/v1/messages");
538
+ });
539
+
540
+ test("streaming multi-turn reconstructs assistant blocks from turn-1 SSE bytes", () => {
541
+ const turn1Sse = [
542
+ "event: message_start",
543
+ 'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[]}}',
544
+ "",
545
+ "event: content_block_start",
546
+ 'data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_sse_1","name":"get_weather","input":{}}}',
547
+ "",
548
+ "event: content_block_delta",
549
+ 'data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\\"location\\":\\"Boston, MA\\"}"}}',
550
+ "",
551
+ "event: content_block_stop",
552
+ 'data: {"type":"content_block_stop","index":0}',
553
+ "",
554
+ "event: message_stop",
555
+ 'data: {"type":"message_stop"}',
556
+ "",
557
+ ].join("\n");
558
+ const turn1Response: CapturedResponse = {
559
+ status: 200,
560
+ headers: {},
561
+ parsed: null,
562
+ bytes: new TextEncoder().encode(turn1Sse),
563
+ };
564
+ const steps = collectSteps({
565
+ model: SONNET,
566
+ capability: "function-calling-multi-turn-streaming",
567
+ responses: [turn1Response],
568
+ });
569
+ expect(steps.length).toBe(2);
570
+ const turn2 = steps[1];
571
+ if (turn2 === undefined || turn2.kind !== "json") {
572
+ throw new Error("expected JSON turn-2");
573
+ }
574
+ const body = expectAnthropicBody(turn2.body);
575
+ const assistantTurn = body.messages[1];
576
+ if (assistantTurn === undefined || assistantTurn.role !== "assistant") {
577
+ throw new Error("turn-2 messages[1] is not the assistant echo");
578
+ }
579
+ expect(assistantTurn.content).toEqual([
580
+ {
581
+ type: "tool_use",
582
+ id: "toolu_sse_1",
583
+ name: "get_weather",
584
+ input: { location: "Boston, MA" },
585
+ },
586
+ ]);
587
+ const userFollowUp = body.messages[2];
588
+ if (userFollowUp === undefined || userFollowUp.role !== "user") {
589
+ throw new Error("turn-2 messages[2] is not the user follow-up");
590
+ }
591
+ const userContent = expectArrayContent(userFollowUp);
592
+ const toolResult = userContent[0];
593
+ if (toolResult === undefined || toolResult.type !== "tool_result") {
594
+ throw new Error("turn-2 user follow-up missing a tool_result block");
595
+ }
596
+ expect(toolResult.tool_use_id).toBe("toolu_sse_1");
597
+ });
598
+
599
+ test("code-execution carries the beta header on the per-step headers map", () => {
600
+ const steps = collectSteps({
601
+ model: SONNET,
602
+ capability: "code-execution",
603
+ responses: [],
604
+ });
605
+ const [only] = steps;
606
+ if (only === undefined) throw new Error("missing step");
607
+ expect(only.headers?.["anthropic-beta"]).toBe("code-execution-2025-05-22");
608
+ });
609
+
610
+ test("plain-text does NOT carry any anthropic-beta header", () => {
611
+ const steps = collectSteps({
612
+ model: SONNET,
613
+ capability: "plain-text",
614
+ responses: [],
615
+ });
616
+ const [only] = steps;
617
+ if (only === undefined) throw new Error("missing step");
618
+ expect(only.headers?.["anthropic-beta"]).toBeUndefined();
619
+ });
620
+ });
621
+
622
+ describe("extractReasoningTrace", () => {
623
+ test("returns the first thinking block with field path and signature", () => {
624
+ const trace = extractReasoningTrace({
625
+ content: [
626
+ {
627
+ type: "thinking",
628
+ thinking: "Let me reason through this step by step.",
629
+ signature: "sig-abc",
630
+ },
631
+ { type: "text", text: "Result." },
632
+ ],
633
+ });
634
+ expect(trace).toEqual({
635
+ blockType: "thinking",
636
+ fieldPath: "content[0].thinking",
637
+ sample: "Let me reason through this step by step.",
638
+ signature: "sig-abc",
639
+ });
640
+ });
641
+
642
+ test("returns the first redacted_thinking block with field path", () => {
643
+ const trace = extractReasoningTrace({
644
+ content: [
645
+ {
646
+ type: "redacted_thinking",
647
+ data: "encrypted-bytes-here",
648
+ signature: "sig-z",
649
+ },
650
+ ],
651
+ });
652
+ expect(trace).toEqual({
653
+ blockType: "redacted_thinking",
654
+ fieldPath: "content[0].data",
655
+ sample: "encrypted-bytes-here",
656
+ signature: "sig-z",
657
+ });
658
+ });
659
+
660
+ test("returns null when no thinking-class block is present", () => {
661
+ expect(
662
+ extractReasoningTrace({
663
+ content: [{ type: "text", text: "no thinking" }],
664
+ }),
665
+ ).toBeNull();
666
+ });
667
+
668
+ test("returns null for a non-object input", () => {
669
+ expect(extractReasoningTrace("not an object")).toBeNull();
670
+ expect(extractReasoningTrace(null)).toBeNull();
671
+ });
672
+ });
673
+
674
+ describe("isSupportedCapability", () => {
675
+ test("recognises everything Anthropic exposes and rejects the rest", () => {
676
+ expect(isSupportedCapability("plain-text")).toBe(true);
677
+ expect(isSupportedCapability("redacted-thinking")).toBe(true);
678
+ expect(isSupportedCapability("audio-input")).toBe(false);
679
+ expect(isSupportedCapability("image-output")).toBe(false);
680
+ });
681
+ });