1import { AnthropicClient, AnthropicLanguageModel, AnthropicTool } from "@effect/ai-anthropic"
2import { assert, describe, it } from "@effect/vitest"
3import { Cause, Effect, Exit, Layer, Option, Redacted, Schema, Stream } from "effect"
4import {
5 type AiError,
6 AnthropicStructuredOutput,
7 LanguageModel,
8 Prompt,
9 Response as AiResponse,
10 Tool,
11 Toolkit
12} from "effect/ai"
13import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/http"
14
15describe("AnthropicLanguageModel", () => {
16 describe("streamText", () => {
17 for (
18 const [label, geo] of [
19 ["missing", {}],
20 ["null", { inference_geo: null }],
21 ["string", { inference_geo: "us" }]
22 ] as const
23 ) {
24 it.effect(`accepts ${label} usage.inference_geo in message_start`, () =>
25 Effect.gen(function*() {
26 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
27 Layer.provide(Layer.succeed(
28 HttpClient.HttpClient,
29 makeHttpClient((request) =>
30 Effect.succeed(sseResponse(request, [
31 {
32 type: "message_start",
33 message: {
34 id: "msg_test_1",
35 type: "message",
36 role: "assistant",
37 model: "claude-sonnet-4-20250514",
38 content: [],
39 stop_reason: null,
40 stop_sequence: null,
41 usage: {
42 cache_creation: null,
43 cache_creation_input_tokens: null,
44 cache_read_input_tokens: null,
45 ...geo,
46 input_tokens: 10,
47 output_tokens: 0,
48 service_tier: null
49 }
50 }
51 },
52 { type: "content_block_start", index: 0, content_block: { type: "text", text: "Hello" } },
53 { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" } },
54 { type: "content_block_stop", index: 0 },
55 {
56 type: "message_delta",
57 delta: { stop_reason: "end_turn", stop_sequence: null },
58 usage: {
59 cache_creation_input_tokens: null,
60 cache_read_input_tokens: null,
61 input_tokens: null,
62 output_tokens: 5
63 }
64 },
65 { type: "message_stop" }
66 ]))
67 )
68 ))
69 )
70
71 const parts = yield* LanguageModel.streamText({ prompt: "Hello" }).pipe(
72 Stream.runCollect,
73 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
74 Effect.provide(layer)
75 )
76 assert.isTrue(
77 globalThis.Array.from(parts).some((part) => part.type === "text-delta" && part.delta === "Hello")
78 )
79 }))
80 }
81
82 it.effect("decodes tool call params in content_block_stop", () =>
83 Effect.gen(function*() {
84 const toolParams = { pattern: "*.ts" }
85
86 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
87 Layer.provide(Layer.succeed(
88 HttpClient.HttpClient,
89 makeHttpClient((request) =>
90 Effect.succeed(sseResponse(request, [
91 {
92 type: "message_start",
93 message: {
94 id: "msg_test_1",
95 type: "message",
96 role: "assistant",
97 model: "claude-sonnet-4-20250514",
98 content: [],
99 stop_reason: null,
100 stop_sequence: null,
101 usage: {
102 cache_creation: null,
103 cache_creation_input_tokens: null,
104 cache_read_input_tokens: null,
105 inference_geo: null,
106 input_tokens: 10,
107 output_tokens: 0,
108 service_tier: null
109 }
110 }
111 },
112 {
113 type: "content_block_start",
114 index: 0,
115 content_block: {
116 type: "tool_use",
117 id: "toolu_test_1",
118 name: "GlobTool",
119 input: {}
120 }
121 },
122 {
123 type: "content_block_delta",
124 index: 0,
125 delta: {
126 type: "input_json_delta",
127 partial_json: JSON.stringify(toolParams)
128 }
129 },
130 {
131 type: "content_block_stop",
132 index: 0
133 },
134 {
135 type: "message_delta",
136 delta: {
137 stop_reason: "tool_use",
138 stop_sequence: null
139 },
140 usage: {
141 cache_creation_input_tokens: null,
142 cache_read_input_tokens: null,
143 input_tokens: null,
144 output_tokens: 5
145 }
146 },
147 {
148 type: "message_stop"
149 }
150 ]))
151 )
152 ))
153 )
154
155 const GlobTool = Tool.make("GlobTool", {
156 description: "Search for files",
157 parameters: Schema.Struct({ pattern: Schema.String }),
158 success: Schema.String
159 })
160
161 const toolkit = Toolkit.make(GlobTool)
162 const toolkitLayer = toolkit.toLayer({
163 GlobTool: () => Effect.succeed("found.ts")
164 })
165
166 const partsChunk = yield* LanguageModel.streamText({
167 prompt: "find ts files",
168 toolkit,
169 disableToolCallResolution: true
170 }).pipe(
171 Stream.runCollect,
172 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
173 Effect.provide(toolkitLayer),
174 Effect.provide(layer)
175 )
176
177 const parts = globalThis.Array.from(partsChunk)
178 const toolCall = parts.find((part) => part.type === "tool-call")
179 assert.isDefined(toolCall)
180 if (toolCall?.type !== "tool-call") {
181 return
182 }
183
184 assert.strictEqual(toolCall.name, "GlobTool")
185 assert.deepStrictEqual(toolCall.params, toolParams)
186 }))
187
188 for (const blockType of ["tool_use", "server_tool_use"] as const) {
189 it.effect(`fails with AiError for malformed streamed ${blockType} JSON`, () =>
190 Effect.gen(function*() {
191 const toolName = blockType === "tool_use" ? "GlobTool" : "web_search"
192 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
193 Layer.provide(Layer.succeed(
194 HttpClient.HttpClient,
195 makeHttpClient((request) =>
196 Effect.succeed(sseResponse(request, [
197 {
198 type: "message_start",
199 message: {
200 id: "msg_test_1",
201 type: "message",
202 role: "assistant",
203 model: "claude-sonnet-4-20250514",
204 content: [],
205 stop_reason: null,
206 stop_sequence: null,
207 usage: {
208 cache_creation: null,
209 cache_creation_input_tokens: null,
210 cache_read_input_tokens: null,
211 inference_geo: null,
212 input_tokens: 10,
213 output_tokens: 0,
214 service_tier: null
215 }
216 }
217 },
218 {
219 type: "content_block_start",
220 index: 0,
221 content_block: {
222 type: blockType,
223 id: blockType === "tool_use" ? "toolu_test_1" : "srvtoolu_test_1",
224 name: toolName,
225 input: {}
226 }
227 },
228 {
229 type: "content_block_delta",
230 index: 0,
231 delta: { type: "input_json_delta", partial_json: "{\"pattern\":" }
232 },
233 {
234 type: "content_block_delta",
235 index: 0,
236 delta: { type: "input_json_delta", partial_json: "\"*.ts" }
237 },
238 { type: "content_block_stop", index: 0 },
239 {
240 type: "message_delta",
241 delta: { stop_reason: "max_tokens", stop_sequence: null },
242 usage: {
243 cache_creation_input_tokens: null,
244 cache_read_input_tokens: null,
245 input_tokens: null,
246 output_tokens: 5
247 }
248 },
249 { type: "message_stop" }
250 ]))
251 )
252 ))
253 )
254 const toolkit = Toolkit.make(
255 Tool.make("GlobTool", { parameters: Schema.Struct({ pattern: Schema.String }) }),
256 AnthropicTool.WebSearch_20250305({})
257 )
258 const exit = yield* LanguageModel.streamText({
259 prompt: "find ts files",
260 toolkit,
261 disableToolCallResolution: true
262 }).pipe(
263 Stream.runCollect,
264 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
265 Effect.provide(layer),
266 Effect.exit
267 )
268
269 assert.isTrue(Exit.isFailure(exit))
270 if (!Exit.isFailure(exit)) {
271 return yield* Effect.die(new Error("Expected malformed tool JSON to fail"))
272 }
273 assert.isFalse(Cause.hasDies(exit.cause))
274 const error = Option.getOrThrow(Cause.findErrorOption(exit.cause))
275 assert.strictEqual(error._tag, "AiError")
276 assert.strictEqual(error.reason._tag, "ToolParameterValidationError")
277 }))
278 }
279
280 it.effect("routes invalid tool call params through failureMode: return without failing the stream", () =>
281 Effect.gen(function*() {
282 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
283 Layer.provide(Layer.succeed(
284 HttpClient.HttpClient,
285 makeHttpClient((request) =>
286 Effect.succeed(sseResponse(request, [
287 {
288 type: "message_start",
289 message: {
290 id: "msg_test_1",
291 type: "message",
292 role: "assistant",
293 model: "claude-sonnet-4-20250514",
294 content: [],
295 stop_reason: null,
296 stop_sequence: null,
297 usage: {
298 cache_creation: null,
299 cache_creation_input_tokens: null,
300 cache_read_input_tokens: null,
301 inference_geo: null,
302 input_tokens: 10,
303 output_tokens: 0,
304 service_tier: null
305 }
306 }
307 },
308 {
309 type: "content_block_start",
310 index: 0,
311 content_block: {
312 type: "tool_use",
313 id: "toolu_test_1",
314 name: "GlobTool",
315 input: {}
316 }
317 },
318 {
319 type: "content_block_delta",
320 index: 0,
321 delta: {
322 type: "input_json_delta",
323 partial_json: JSON.stringify({ pattern: 123 })
324 }
325 },
326 {
327 type: "content_block_stop",
328 index: 0
329 },
330 {
331 type: "message_delta",
332 delta: {
333 stop_reason: "tool_use",
334 stop_sequence: null
335 },
336 usage: {
337 cache_creation_input_tokens: null,
338 cache_read_input_tokens: null,
339 input_tokens: null,
340 output_tokens: 5
341 }
342 },
343 {
344 type: "message_stop"
345 }
346 ]))
347 )
348 ))
349 )
350
351 const GlobTool = Tool.make("GlobTool", {
352 description: "Search for files",
353 failureMode: "return",
354 parameters: Schema.Struct({ pattern: Schema.String }),
355 success: Schema.String,
356 failure: Schema.String
357 })
358
359 const toolkit = Toolkit.make(GlobTool)
360 const toolkitLayer = toolkit.toLayer({
361 GlobTool: () => Effect.succeed("found.ts")
362 })
363
364 const partsChunk = yield* LanguageModel.streamText({
365 prompt: "find ts files",
366 toolkit
367 }).pipe(
368 Stream.runCollect,
369 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
370 Effect.provide(toolkitLayer),
371 Effect.provide(layer)
372 )
373
374 const parts = globalThis.Array.from(partsChunk)
375 const toolResult = parts.find((part) => part.type === "tool-result")
376 assert.isDefined(toolResult)
377 if (toolResult?.type !== "tool-result") {
378 return
379 }
380
381 assert.strictEqual(toolResult.isFailure, true)
382 const failure = toolResult.result as AiError.AiError
383 assert.strictEqual(failure._tag, "AiError")
384 assert.strictEqual(failure.reason._tag, "ToolParameterValidationError")
385 }))
386
387 const codeExecutionCases = [
388 {
389 providerName: "bash_code_execution",
390 toolParams: { command: "pwd" },
391 expectedParams: { type: "bash_code_execution", command: "pwd" }
392 },
393 {
394 providerName: "text_editor_code_execution",
395 toolParams: { command: "view", path: "/tmp/example.txt" },
396 expectedParams: {
397 type: "text_editor_code_execution",
398 command: "view",
399 path: "/tmp/example.txt"
400 }
401 }
402 ] as const
403
404 for (const { expectedParams, providerName, toolParams } of codeExecutionCases) {
405 it.effect(`emits valid JSON for streamed ${providerName} parameters`, () =>
406 Effect.gen(function*() {
407 const toolkit = Toolkit.make(AnthropicTool.CodeExecution_20250522())
408 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
409 Layer.provide(Layer.succeed(
410 HttpClient.HttpClient,
411 makeHttpClient((request) =>
412 Effect.succeed(sseResponse(request, [
413 {
414 type: "message_start",
415 message: {
416 id: "msg_test_1",
417 type: "message",
418 role: "assistant",
419 model: "claude-sonnet-4-20250514",
420 content: [],
421 stop_reason: null,
422 stop_sequence: null,
423 usage: {
424 cache_creation: null,
425 cache_creation_input_tokens: null,
426 cache_read_input_tokens: null,
427 inference_geo: null,
428 input_tokens: 1,
429 output_tokens: 0,
430 service_tier: null
431 }
432 }
433 },
434 {
435 type: "content_block_start",
436 index: 0,
437 content_block: {
438 type: "server_tool_use",
439 id: "srvtoolu_test_1",
440 name: providerName,
441 input: {}
442 }
443 },
444 {
445 type: "content_block_delta",
446 index: 0,
447 delta: {
448 type: "input_json_delta",
449 partial_json: JSON.stringify(toolParams)
450 }
451 },
452 {
453 type: "content_block_stop",
454 index: 0
455 },
456 {
457 type: "message_delta",
458 delta: {
459 stop_reason: "tool_use",
460 stop_sequence: null
461 },
462 usage: {
463 cache_creation_input_tokens: null,
464 cache_read_input_tokens: null,
465 input_tokens: null,
466 output_tokens: 1
467 }
468 },
469 {
470 type: "message_stop"
471 }
472 ]))
473 )
474 ))
475 )
476
477 const partsChunk = yield* LanguageModel.streamText({
478 prompt: "run pwd",
479 toolkit,
480 disableToolCallResolution: true
481 }).pipe(
482 Stream.runCollect,
483 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
484 Effect.provide(layer)
485 )
486
487 const parts = globalThis.Array.from(partsChunk)
488
489 const delta = parts.find((part) => part.type === "tool-params-delta")
490 assert.isDefined(delta)
491 if (delta?.type === "tool-params-delta") {
492 assert.deepStrictEqual(JSON.parse(delta.delta), expectedParams)
493 }
494
495 const toolCall = parts.find((part) => part.type === "tool-call")
496 assert.isDefined(toolCall)
497 if (toolCall?.type === "tool-call") {
498 assert.deepStrictEqual(toolCall.params, expectedParams)
499 }
500 }))
501 }
502
503 // `Model` is an open enum in Anthropic's spec (`anyOf: [{ type: string }, ...consts]`), and it is
504 // $ref'd by response schemas. Responses must therefore decode for model ids that are newer than the
505 // generated literals, and the id must survive decoding unchanged.
506 it.effect("decodes responses for a model id that is not a known literal", () =>
507 Effect.gen(function*() {
508 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
509 Layer.provide(Layer.succeed(
510 HttpClient.HttpClient,
511 makeHttpClient((request) =>
512 Effect.succeed(sseResponse(request, [
513 {
514 type: "message_start",
515 message: {
516 id: "msg_test_1",
517 type: "message",
518 role: "assistant",
519 model: "claude-not-a-known-model-id",
520 content: [],
521 stop_reason: null,
522 stop_sequence: null,
523 usage: {
524 cache_creation: null,
525 cache_creation_input_tokens: null,
526 cache_read_input_tokens: null,
527 inference_geo: null,
528 input_tokens: 10,
529 output_tokens: 0,
530 service_tier: null
531 }
532 }
533 },
534 {
535 type: "content_block_start",
536 index: 0,
537 content_block: { type: "text", text: "" }
538 },
539 {
540 type: "content_block_delta",
541 index: 0,
542 delta: { type: "text_delta", text: "Hello" }
543 },
544 {
545 type: "content_block_stop",
546 index: 0
547 },
548 {
549 type: "message_delta",
550 delta: {
551 stop_reason: "end_turn",
552 stop_sequence: null
553 },
554 usage: {
555 cache_creation_input_tokens: null,
556 cache_read_input_tokens: null,
557 input_tokens: null,
558 output_tokens: 5
559 }
560 },
561 {
562 type: "message_stop"
563 }
564 ]))
565 )
566 ))
567 )
568
569 const partsChunk = yield* LanguageModel.streamText({
570 prompt: "say hello"
571 }).pipe(
572 Stream.runCollect,
573 Effect.provide(AnthropicLanguageModel.model("claude-not-a-known-model-id")),
574 Effect.provide(layer)
575 )
576
577 const parts = globalThis.Array.from(partsChunk)
578 const metadata = parts.find((part) => part.type === "response-metadata")
579 assert.isDefined(metadata)
580 if (metadata?.type !== "response-metadata") {
581 return
582 }
583
584 assert.strictEqual(metadata.modelId, "claude-not-a-known-model-id")
585
586 const text = parts.find((part) => part.type === "text-delta")
587 assert.isDefined(text)
588 if (text?.type !== "text-delta") {
589 return
590 }
591
592 assert.strictEqual(text.delta, "Hello")
593 }))
594 })
595
596 describe("generateText", () => {
597 for (
598 const [label, geo] of [
599 ["missing", {}],
600 ["null", { inference_geo: null }],
601 ["string", { inference_geo: "us" }]
602 ] as const
603 ) {
604 it.effect(`accepts ${label} usage.inference_geo in a message response`, () =>
605 Effect.gen(function*() {
606 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
607 Layer.provide(Layer.succeed(
608 HttpClient.HttpClient,
609 makeHttpClient((request) =>
610 Effect.succeed(jsonResponse(request, {
611 id: "msg_test_1",
612 type: "message",
613 role: "assistant",
614 model: "claude-sonnet-4-20250514",
615 content: [{ type: "text", text: "Hello" }],
616 stop_reason: "end_turn",
617 stop_sequence: null,
618 usage: {
619 cache_creation: null,
620 cache_creation_input_tokens: null,
621 cache_read_input_tokens: null,
622 ...geo,
623 input_tokens: 10,
624 output_tokens: 5,
625 service_tier: null
626 }
627 }))
628 )
629 ))
630 )
631
632 const response = yield* LanguageModel.generateText({ prompt: "Hello" }).pipe(
633 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
634 Effect.provide(layer)
635 )
636 assert.isTrue(response.content.some((part) => part.type === "text" && part.text === "Hello"))
637 }))
638 }
639
640 it.effect("omits strictJsonSchema from the request while preserving tool strictness", () =>
641 Effect.gen(function*() {
642 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
643 const model = "claude-sonnet-4-5"
644 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
645 Layer.provide(Layer.succeed(
646 HttpClient.HttpClient,
647 makeHttpClient((request) => {
648 capturedRequest = request
649 return Effect.succeed(jsonResponse(request, {
650 id: "msg_test_1",
651 type: "message",
652 role: "assistant",
653 model,
654 content: [{ type: "text", text: "Hello" }],
655 stop_reason: "end_turn",
656 stop_sequence: null,
657 usage: {
658 cache_creation: null,
659 cache_creation_input_tokens: null,
660 cache_read_input_tokens: null,
661 inference_geo: null,
662 input_tokens: 1,
663 output_tokens: 1,
664 service_tier: null
665 }
666 }))
667 })
668 ))
669 )
670
671 yield* LanguageModel.generateText({
672 prompt: "Hello",
673 toolkit: Toolkit.make(Tool.make("Search", {
674 parameters: Schema.Struct({ query: Schema.String }),
675 success: Schema.String
676 })),
677 disableToolCallResolution: true
678 }).pipe(
679 Effect.provide(AnthropicLanguageModel.model(model, { strictJsonSchema: false })),
680 Effect.provide(layer)
681 )
682
683 assert.isDefined(capturedRequest)
684 const body = yield* getRequestBody(capturedRequest)
685 assert.strictEqual(body.model, model)
686 assert.strictEqual(body.tools[0].name, "Search")
687 assert.strictEqual(body.tools[0].strict, false)
688 assert.notProperty(body, "strictJsonSchema")
689 }))
690
691 it.effect("preserves string tool results", () =>
692 Effect.gen(function*() {
693 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
694 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
695 Layer.provide(Layer.succeed(
696 HttpClient.HttpClient,
697 makeHttpClient((request) => {
698 capturedRequest = request
699 return Effect.succeed(jsonResponse(request, {
700 id: "msg_test_1",
701 type: "message",
702 role: "assistant",
703 model: "claude-sonnet-4-20250514",
704 content: [{ type: "text", text: "Done" }],
705 stop_reason: "end_turn",
706 stop_sequence: null,
707 usage: {
708 cache_creation: null,
709 cache_creation_input_tokens: null,
710 cache_read_input_tokens: null,
711 inference_geo: null,
712 input_tokens: 10,
713 output_tokens: 5,
714 service_tier: null
715 }
716 }))
717 })
718 ))
719 )
720
721 yield* LanguageModel.generateText({
722 prompt: Prompt.make([
723 { role: "user", content: "Use the tool" },
724 {
725 role: "assistant",
726 content: [Prompt.toolCallPart({
727 id: "call_text",
728 name: "text_tool",
729 params: {},
730 providerExecuted: false
731 })]
732 },
733 {
734 role: "tool",
735 content: [Prompt.toolResultPart({
736 id: "call_text",
737 name: "text_tool",
738 result: "PLAIN_TEXT_SENTINEL\n",
739 isFailure: false,
740 providerExecuted: false
741 })]
742 }
743 ]),
744 disableToolCallResolution: true
745 }).pipe(
746 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
747 Effect.provide(layer)
748 )
749
750 assert.isDefined(capturedRequest)
751 if (capturedRequest === undefined) {
752 return
753 }
754
755 const body = yield* getRequestBody(capturedRequest)
756 const toolResult = body.messages
757 .flatMap((message: any) => Array.isArray(message.content) ? message.content : [])
758 .find((block: any) => block.type === "tool_result")
759
760 assert.isDefined(toolResult)
761 assert.strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n")
762 }))
763
764 it.effect("preserves base64 image string payloads", () =>
765 Effect.gen(function*() {
766 const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII="
767 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
768 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
769 Layer.provide(Layer.succeed(
770 HttpClient.HttpClient,
771 makeHttpClient((request) => {
772 capturedRequest = request
773 return Effect.succeed(jsonResponse(request, {
774 id: "msg_test_1",
775 type: "message",
776 role: "assistant",
777 model: "claude-sonnet-4-20250514",
778 content: [{ type: "text", text: "Done" }],
779 stop_reason: "end_turn",
780 stop_sequence: null,
781 usage: {
782 cache_creation: null,
783 cache_creation_input_tokens: null,
784 cache_read_input_tokens: null,
785 inference_geo: null,
786 input_tokens: 1,
787 output_tokens: 1,
788 service_tier: null
789 }
790 }))
791 })
792 ))
793 )
794
795 yield* LanguageModel.generateText({
796 prompt: Prompt.make([Prompt.userMessage({
797 content: [Prompt.filePart({ mediaType: "image/png", data: base64 })]
798 })])
799 }).pipe(
800 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
801 Effect.provide(layer)
802 )
803
804 assert.isDefined(capturedRequest)
805 const body = yield* getRequestBody(capturedRequest)
806 assert.strictEqual(body.messages[0].content[0].source.data, base64)
807 }))
808
809 it.effect("encodes dynamic tools", () =>
810 Effect.gen(function*() {
811 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
812 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
813 Layer.provide(Layer.succeed(
814 HttpClient.HttpClient,
815 makeHttpClient((request) => {
816 capturedRequest = request
817 return Effect.succeed(jsonResponse(request, {
818 id: "msg_test_1",
819 type: "message",
820 role: "assistant",
821 model: "claude-sonnet-4-20250514",
822 content: [{ type: "text", text: "Done" }],
823 stop_reason: "end_turn",
824 stop_sequence: null,
825 usage: {
826 cache_creation: null,
827 cache_creation_input_tokens: null,
828 cache_read_input_tokens: null,
829 inference_geo: null,
830 input_tokens: 10,
831 output_tokens: 5,
832 service_tier: null
833 }
834 }))
835 })
836 ))
837 )
838
839 const inputSchema = {
840 type: "object",
841 properties: {
842 query: { type: "string" },
843 limit: { type: "number" }
844 },
845 required: ["query"],
846 additionalProperties: false
847 } as const
848
849 const DynamicTool = Tool.dynamic("DynamicTool", {
850 description: "A dynamic tool",
851 parameters: inputSchema
852 })
853
854 yield* LanguageModel.generateText({
855 prompt: "Use the dynamic tool",
856 toolkit: Toolkit.make(DynamicTool),
857 disableToolCallResolution: true
858 }).pipe(
859 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
860 Effect.provide(layer)
861 )
862
863 assert.isDefined(capturedRequest)
864 if (capturedRequest === undefined) {
865 return
866 }
867
868 const body = yield* getRequestBody(capturedRequest)
869 const dynamicTool = body.tools.find((tool: any) => tool.name === "DynamicTool")
870
871 assert.isDefined(dynamicTool)
872 if (dynamicTool === undefined) {
873 return
874 }
875
876 assert.strictEqual(dynamicTool.description, "A dynamic tool")
877 assert.deepStrictEqual(dynamicTool.input_schema, inputSchema)
878 }))
879
880 it.effect("serializes provider-executed web_search parts from the assistant message", () =>
881 Effect.gen(function*() {
882 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
883 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
884 Layer.provide(Layer.succeed(
885 HttpClient.HttpClient,
886 makeHttpClient((request) => {
887 capturedRequest = request
888 return Effect.succeed(jsonResponse(request, {
889 id: "msg_test_2",
890 type: "message",
891 role: "assistant",
892 model: "claude-sonnet-4-20250514",
893 content: [{ type: "text", text: "You're welcome" }],
894 stop_reason: "end_turn",
895 stop_sequence: null,
896 usage: {
897 cache_creation: null,
898 cache_creation_input_tokens: null,
899 cache_read_input_tokens: null,
900 inference_geo: null,
901 input_tokens: 10,
902 output_tokens: 5,
903 service_tier: null
904 }
905 }))
906 })
907 ))
908 )
909
910 const searchResults = [{
911 type: "web_search_result",
912 url: "https://example.com/gold",
913 title: "Gold price",
914 encrypted_content: "encrypted",
915 page_age: null
916 }]
917
918 const history = Prompt.fromResponseParts([
919 AiResponse.makePart("tool-call", {
920 id: "srvtoolu_1",
921 name: "AnthropicWebSearch",
922 params: { query: "gold price today" },
923 providerExecuted: true
924 }),
925 AiResponse.makePart("tool-result", {
926 id: "srvtoolu_1",
927 name: "AnthropicWebSearch",
928 isFailure: false,
929 result: searchResults,
930 encodedResult: searchResults,
931 preliminary: false,
932 providerExecuted: true
933 }),
934 AiResponse.makePart("text", { text: "Gold is around $4,000." })
935 ])
936
937 const prompt = Prompt.concat(
938 Prompt.concat(Prompt.make("what is the gold price?"), history),
939 Prompt.make("thanks")
940 )
941
942 yield* LanguageModel.generateText({
943 prompt,
944 toolkit: Toolkit.make(AnthropicTool.WebSearch_20250305({})),
945 disableToolCallResolution: true
946 }).pipe(
947 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
948 Effect.provide(layer)
949 )
950
951 assert.isDefined(capturedRequest)
952 if (capturedRequest === undefined) {
953 return
954 }
955
956 const body = yield* getRequestBody(capturedRequest)
957 const assistantMessage = body.messages.find((message: any) => message.role === "assistant")
958 assert.isDefined(assistantMessage)
959
960 const serverToolUse = assistantMessage.content.find((block: any) => block.type === "server_tool_use")
961 assert.isDefined(serverToolUse)
962 assert.strictEqual(serverToolUse.id, "srvtoolu_1")
963 assert.strictEqual(serverToolUse.name, "web_search")
964
965 const searchResult = assistantMessage.content.find((block: any) => block.type === "web_search_tool_result")
966 assert.isDefined(searchResult)
967 assert.strictEqual(searchResult.tool_use_id, "srvtoolu_1")
968
969 const clientToolResults = body.messages.flatMap((message: any) =>
970 Array.isArray(message.content)
971 ? message.content.filter((block: any) => block.type === "tool_result")
972 : []
973 )
974 assert.strictEqual(clientToolResults.length, 0)
975 }))
976
977 it.effect("encodes plaintext bytes as UTF-8 text", () =>
978 Effect.gen(function*() {
979 let body: any
980 const client = AnthropicClient.layer({ apiKey: Redacted.make("test") }).pipe(
981 Layer.provide(Layer.succeed(
982 HttpClient.HttpClient,
983 HttpClient.makeWith(
984 Effect.fnUntraced(function*(requestEffect) {
985 const request = yield* requestEffect
986 body = JSON.parse(new TextDecoder().decode((request.body as any).body))
987 return HttpClientResponse.fromWeb(
988 request,
989 new Response(
990 JSON.stringify({
991 id: "msg_1",
992 type: "message",
993 role: "assistant",
994 model: "claude-sonnet-4-20250514",
995 content: [{ type: "text", text: "ok" }],
996 stop_reason: "end_turn",
997 stop_sequence: null,
998 usage: {
999 cache_creation: null,
1000 cache_creation_input_tokens: null,
1001 cache_read_input_tokens: null,
1002 inference_geo: null,
1003 input_tokens: 1,
1004 output_tokens: 1,
1005 service_tier: null
1006 }
1007 }),
1008 { status: 200 }
1009 )
1010 )
1011 }),
1012 Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
1013 )
1014 ))
1015 )
1016
1017 yield* LanguageModel.generateText({
1018 prompt: Prompt.make([{
1019 role: "user",
1020 content: [Prompt.filePart({ mediaType: "text/plain", data: new TextEncoder().encode("hello") })]
1021 }])
1022 }).pipe(
1023 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
1024 Effect.provide(client)
1025 )
1026
1027 assert.strictEqual(body.messages[0].content[0].source.data, "hello")
1028 }))
1029 })
1030
1031 describe("generateObject", () => {
1032 const getRequest = (
1033 model: string,
1034 config?: { readonly structuredOutputs?: boolean | undefined }
1035 ) =>
1036 Effect.gen(function*() {
1037 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
1038 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1039 Layer.provide(Layer.succeed(
1040 HttpClient.HttpClient,
1041 makeHttpClient((request) => {
1042 capturedRequest = request
1043 return Effect.succeed(jsonResponse(request, {
1044 id: "msg_test_1",
1045 type: "message",
1046 role: "assistant",
1047 model,
1048 content: [{ type: "text", text: JSON.stringify({ name: "John", age: 30 }) }],
1049 stop_reason: "end_turn",
1050 stop_sequence: null,
1051 usage: {
1052 cache_creation: null,
1053 cache_creation_input_tokens: null,
1054 cache_read_input_tokens: null,
1055 inference_geo: null,
1056 input_tokens: 10,
1057 output_tokens: 5,
1058 service_tier: null
1059 }
1060 }))
1061 })
1062 ))
1063 )
1064
1065 // Assert the request shape; the response outcome is irrelevant here.
1066 yield* LanguageModel.generateObject({
1067 prompt: "Give me a person",
1068 schema: Schema.Struct({ name: Schema.String, age: Schema.Number })
1069 }).pipe(
1070 Effect.provide(AnthropicLanguageModel.model(model, config)),
1071 Effect.provide(layer),
1072 Effect.ignore
1073 )
1074
1075 assert.isDefined(capturedRequest)
1076 if (capturedRequest === undefined) {
1077 return yield* Effect.die(new Error("Expected a captured request"))
1078 }
1079
1080 return yield* getRequestBody(capturedRequest)
1081 })
1082
1083 it.effect("uses native structured output and 128K for Claude 4.6", () =>
1084 Effect.gen(function*() {
1085 const body = yield* getRequest("claude-opus-4-6")
1086
1087 assert.strictEqual(body.max_tokens, 128000)
1088 assert.strictEqual(body.output_config?.format?.type, "json_schema")
1089 }))
1090
1091 it.effect("uses optimistic modern defaults for an unknown future model", () =>
1092 Effect.gen(function*() {
1093 const body = yield* getRequest("claude-sonnet-6-0")
1094
1095 assert.strictEqual(body.max_tokens, 128000)
1096 assert.strictEqual(body.output_config?.format?.type, "json_schema")
1097 }))
1098
1099 it.effect("preserves frozen legacy model exceptions", () =>
1100 Effect.gen(function*() {
1101 const body = yield* getRequest("claude-sonnet-4-20250514")
1102
1103 assert.strictEqual(body.max_tokens, 64000)
1104 assert.isUndefined(body.output_config)
1105 }))
1106
1107 it.effect("can disable structured outputs for a modern model", () =>
1108 Effect.gen(function*() {
1109 const body = yield* getRequest("claude-sonnet-6-0", { structuredOutputs: false })
1110
1111 assert.strictEqual(body.max_tokens, 128000)
1112 assert.isUndefined(body.output_config)
1113 assert.notProperty(body, "structuredOutputs")
1114 }))
1115
1116 it.effect("can enable structured outputs for a legacy model", () =>
1117 Effect.gen(function*() {
1118 const body = yield* getRequest("claude-sonnet-4-20250514", { structuredOutputs: true })
1119
1120 assert.strictEqual(body.max_tokens, 64000)
1121 assert.strictEqual(body.output_config?.format?.type, "json_schema")
1122 assert.notProperty(body, "structuredOutputs")
1123 }))
1124
1125 const summaryResponse = (request: HttpClientRequest.HttpClientRequest, content: ReadonlyArray<unknown>) =>
1126 jsonResponse(request, {
1127 id: "msg_summary",
1128 type: "message",
1129 role: "assistant",
1130 model: "claude-sonnet-4-6",
1131 content,
1132 stop_reason: "tool_use",
1133 stop_sequence: null,
1134 usage: {
1135 cache_creation: null,
1136 cache_creation_input_tokens: null,
1137 cache_read_input_tokens: null,
1138 inference_geo: null,
1139 input_tokens: 1,
1140 output_tokens: 1,
1141 service_tier: null
1142 }
1143 })
1144
1145 it.effect("forces the fallback response tool and decodes its input alongside prose", () =>
1146 Effect.gen(function*() {
1147 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1148 Layer.provide(Layer.succeed(
1149 HttpClient.HttpClient,
1150 makeHttpClient((request) =>
1151 Effect.gen(function*() {
1152 const body = yield* getRequestBody(request)
1153 assert.deepStrictEqual(body.tools?.map((tool: { name: string }) => tool.name), ["summary"])
1154 assert.deepStrictEqual(body.tool_choice, {
1155 type: "tool",
1156 name: "summary",
1157 disable_parallel_tool_use: true
1158 })
1159 return summaryResponse(request, [
1160 { type: "text", text: "Here is the summary." },
1161 { type: "tool_use", id: "toolu_summary", name: "summary", input: { title: "Rain" } }
1162 ])
1163 })
1164 )
1165 ))
1166 )
1167
1168 const response = yield* LanguageModel.generateObject({
1169 prompt: "Give a title for a story about rain.",
1170 objectName: "summary",
1171 schema: Schema.Struct({ title: Schema.String })
1172 }).pipe(
1173 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6", { structuredOutputs: false })),
1174 Effect.provide(layer)
1175 )
1176
1177 assert.deepStrictEqual(response.value, { title: "Rain" })
1178 }))
1179
1180 it.effect("decodes native JSON alongside an ordinary tool call", () =>
1181 Effect.gen(function*() {
1182 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1183 Layer.provide(Layer.succeed(
1184 HttpClient.HttpClient,
1185 makeHttpClient((request) =>
1186 Effect.succeed(summaryResponse(request, [
1187 { type: "text", text: JSON.stringify({ title: "Rain" }) },
1188 { type: "tool_use", id: "toolu_weather", name: "Weather", input: { city: "SF" } }
1189 ]))
1190 )
1191 ))
1192 )
1193 const toolkit = Toolkit.make(Tool.make("Weather", {
1194 parameters: Schema.Struct({ city: Schema.String }),
1195 success: Schema.String
1196 }))
1197
1198 const response = yield* LanguageModel.generateObject({
1199 prompt: "Give a title for a story about rain.",
1200 objectName: "summary",
1201 schema: Schema.Struct({ title: Schema.String }),
1202 toolkit
1203 }).pipe(
1204 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-6")),
1205 Effect.provide(toolkit.toLayer({ Weather: () => Effect.succeed("Rain") })),
1206 Effect.provide(layer)
1207 )
1208
1209 assert.deepStrictEqual(response.value, { title: "Rain" })
1210 assert.strictEqual(response.toolCalls[0]?.name, "Weather")
1211 }))
1212 })
1213
1214 describe("system messages", () => {
1215 const getRequest = (
1216 model: string,
1217 prompt: Prompt.RawInput,
1218 config?: { readonly midConversationSystemMessages?: boolean | undefined }
1219 ) =>
1220 Effect.gen(function*() {
1221 let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined
1222 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1223 Layer.provide(Layer.succeed(
1224 HttpClient.HttpClient,
1225 makeHttpClient((request) => {
1226 capturedRequest = request
1227 return Effect.succeed(jsonResponse(request, {
1228 id: "msg_test_1",
1229 type: "message",
1230 role: "assistant",
1231 model,
1232 content: [{ type: "text", text: "Done" }],
1233 stop_reason: "end_turn",
1234 stop_sequence: null,
1235 usage: {
1236 cache_creation: null,
1237 cache_creation_input_tokens: null,
1238 cache_read_input_tokens: null,
1239 inference_geo: null,
1240 input_tokens: 1,
1241 output_tokens: 1,
1242 service_tier: null
1243 }
1244 }))
1245 })
1246 ))
1247 )
1248
1249 yield* LanguageModel.generateText({ prompt, disableToolCallResolution: true }).pipe(
1250 Effect.provide(AnthropicLanguageModel.model(model, config)),
1251 Effect.provide(layer)
1252 )
1253
1254 if (capturedRequest === undefined) {
1255 return yield* Effect.die(new Error("Expected a captured request"))
1256 }
1257 return yield* getRequestBody(capturedRequest)
1258 })
1259
1260 const conversation = Prompt.make([
1261 { role: "system", content: "A" },
1262 { role: "user", content: "Question 1" },
1263 { role: "assistant", content: "Answer 1" },
1264 { role: "system", content: "B" },
1265 { role: "user", content: "Question 2" }
1266 ])
1267
1268 it.effect("sends later system messages after the next user turn", () =>
1269 Effect.gen(function*() {
1270 const body = yield* getRequest("claude-sonnet-5-5", conversation)
1271
1272 assert.deepStrictEqual(body.system.map((block: any) => block.text), ["A"])
1273 assert.deepStrictEqual(body.messages.map((message: any) => message.role), [
1274 "user",
1275 "assistant",
1276 "user",
1277 "system"
1278 ])
1279 assert.deepStrictEqual(body.messages.at(-1), {
1280 role: "system",
1281 content: [{ type: "text", text: "B", cache_control: null }]
1282 })
1283 }))
1284
1285 it.effect("sends every system message top-level when disabled", () =>
1286 Effect.gen(function*() {
1287 const body = yield* getRequest("claude-sonnet-5-5", conversation, { midConversationSystemMessages: false })
1288
1289 assert.deepStrictEqual(body.system.map((block: any) => block.text), ["A", "B"])
1290 assert.deepStrictEqual(body.messages.map((message: any) => message.role), ["user", "assistant", "user"])
1291 assert.notProperty(body, "midConversationSystemMessages")
1292 }))
1293
1294 it.effect("sends every system message top-level in order when one cannot follow a user turn", () =>
1295 Effect.gen(function*() {
1296 const body = yield* getRequest(
1297 "claude-sonnet-5-5",
1298 Prompt.make([
1299 { role: "system", content: "A" },
1300 { role: "user", content: "Question" },
1301 { role: "system", content: "B" },
1302 { role: "assistant", content: "Answer" },
1303 { role: "system", content: "C" }
1304 ])
1305 )
1306
1307 assert.deepStrictEqual(body.system.map((block: any) => block.text), ["A", "B", "C"])
1308 assert.deepStrictEqual(body.messages.map((message: any) => message.role), ["user", "assistant"])
1309 }))
1310
1311 it.effect("reports support for the requested model", () =>
1312 Effect.gen(function*() {
1313 const model = yield* LanguageModel.LanguageModel
1314
1315 assert.isTrue(yield* model.supportsSystemMessagesInHistory!)
1316 assert.isFalse(
1317 yield* model.supportsSystemMessagesInHistory!.pipe(
1318 AnthropicLanguageModel.withConfigOverride({ model: "claude-sonnet-5" })
1319 )
1320 )
1321 }).pipe(
1322 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-5-5")),
1323 Effect.provide(AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") })),
1324 Effect.provide(Layer.succeed(HttpClient.HttpClient, makeHttpClient(() => Effect.die("unexpected request"))))
1325 ))
1326 })
1327
1328 // The packaged `Memory_20250818` tool ships `customName: "AnthropicMemory"` /
1329 // `providerName: "memory"`, and is a client-executed provider tool. These
1330 // tests cover the round-trip that was broken on beta.98 (see #2615):
1331 // - the provider wire name ("memory") must resolve to the toolkit's custom
1332 // name ("AnthropicMemory"), otherwise `makeResponse` raises ToolNotFound
1333 // - `view_range` uses `Schema.optionalKey`, otherwise the Anthropic codec
1334 // rejects the tool schema with "Unsupported AST Undefined"
1335 // - `create` must carry `file_text`, otherwise the file body is dropped
1336 describe("Memory tool", () => {
1337 const memoryResponse = (request: HttpClientRequest.HttpClientRequest, input: unknown) =>
1338 jsonResponse(request, {
1339 id: "msg_test_1",
1340 type: "message",
1341 role: "assistant",
1342 model: "claude-sonnet-4-20250514",
1343 content: [{ type: "tool_use", id: "toolu_mem_1", name: "memory", input }],
1344 stop_reason: "tool_use",
1345 stop_sequence: null,
1346 usage: {
1347 cache_creation: null,
1348 cache_creation_input_tokens: null,
1349 cache_read_input_tokens: null,
1350 inference_geo: null,
1351 input_tokens: 10,
1352 output_tokens: 5,
1353 service_tier: null
1354 }
1355 })
1356
1357 it.effect("resolves the provider wire name to the tool's custom name (view)", () =>
1358 Effect.gen(function*() {
1359 let receivedParams: unknown = undefined
1360 const toolkit = Toolkit.make(AnthropicTool.Memory_20250818({}))
1361 const toolkitLayer = toolkit.toLayer({
1362 AnthropicMemory: (params) => {
1363 receivedParams = params
1364 return Effect.succeed("memory listing")
1365 }
1366 })
1367
1368 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1369 Layer.provide(Layer.succeed(
1370 HttpClient.HttpClient,
1371 makeHttpClient((request) => Effect.succeed(memoryResponse(request, { command: "view", path: "/memories" })))
1372 ))
1373 )
1374
1375 const response = yield* LanguageModel.generateText({
1376 prompt: "check memory",
1377 toolkit
1378 }).pipe(
1379 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
1380 Effect.provide(toolkitLayer),
1381 Effect.provide(layer)
1382 )
1383
1384 // Handler was resolved under the custom name and received the decoded command
1385 assert.deepStrictEqual(receivedParams, { command: "view", path: "/memories" })
1386
1387 const toolResult = response.toolResults[0]
1388 assert.isDefined(toolResult)
1389 assert.strictEqual(toolResult.name, "AnthropicMemory")
1390 assert.strictEqual(toolResult.result, "memory listing")
1391 }))
1392
1393 it.effect("decodes the create command including file_text", () =>
1394 Effect.gen(function*() {
1395 let receivedParams: unknown = undefined
1396 const toolkit = Toolkit.make(AnthropicTool.Memory_20250818({}))
1397 const toolkitLayer = toolkit.toLayer({
1398 AnthropicMemory: (params) => {
1399 receivedParams = params
1400 return Effect.succeed("File created successfully at: /memories/notes.txt")
1401 }
1402 })
1403
1404 const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
1405 Layer.provide(Layer.succeed(
1406 HttpClient.HttpClient,
1407 makeHttpClient((request) =>
1408 Effect.succeed(memoryResponse(request, {
1409 command: "create",
1410 path: "/memories/notes.txt",
1411 file_text: "hello world"
1412 }))
1413 )
1414 ))
1415 )
1416
1417 const response = yield* LanguageModel.generateText({
1418 prompt: "save a note",
1419 toolkit
1420 }).pipe(
1421 Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")),
1422 Effect.provide(toolkitLayer),
1423 Effect.provide(layer)
1424 )
1425
1426 assert.deepStrictEqual(receivedParams, {
1427 command: "create",
1428 path: "/memories/notes.txt",
1429 file_text: "hello world"
1430 })
1431 assert.strictEqual(response.toolResults[0]?.name, "AnthropicMemory")
1432 }))
1433 })
1434
1435 // Client-executed (`requiresHandler`) provider tools have their `parameters`
1436 // decoded via `toCodecAnthropic` when the model calls them. Optional
1437 // parameters must use `Schema.optionalKey` (not `Schema.optional`), otherwise
1438 // the codec rejects the schema with "Unsupported AST Undefined" (see #2615).
1439 describe("client provider tool parameters compile with the Anthropic codec", () => {
1440 const displayArgs = { displayWidthPx: 800, displayHeightPx: 600 }
1441 const clientTools: ReadonlyArray<readonly [string, Tool.AnyProviderDefined]> = [
1442 ["Bash_20241022", AnthropicTool.Bash_20241022({})],
1443 ["Bash_20250124", AnthropicTool.Bash_20250124({})],
1444 ["ComputerUse_20241022", AnthropicTool.ComputerUse_20241022(displayArgs)],
1445 ["ComputerUse_20250124", AnthropicTool.ComputerUse_20250124(displayArgs)],
1446 ["ComputerUse_20251124", AnthropicTool.ComputerUse_20251124(displayArgs)],
1447 ["Memory_20250818", AnthropicTool.Memory_20250818({})],
1448 ["TextEditor_20241022", AnthropicTool.TextEditor_20241022({})],
1449 ["TextEditor_20250124", AnthropicTool.TextEditor_20250124({})],
1450 ["TextEditor_20250429", AnthropicTool.TextEditor_20250429({})],
1451 ["TextEditor_20250728", AnthropicTool.TextEditor_20250728({})]
1452 ]
1453
1454 for (const [name, tool] of clientTools) {
1455 it(name, () => {
1456 const codec = AnthropicStructuredOutput.toCodecAnthropic(tool.parametersSchema)
1457 assert.isDefined(codec)
1458 })
1459 }
1460 })
1461})
1462
1463const makeHttpClient = (
1464 handler: (
1465 request: HttpClientRequest.HttpClientRequest
1466 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>
1467) =>
1468 HttpClient.makeWith(
1469 Effect.fnUntraced(function*(requestEffect) {
1470 const request = yield* requestEffect
1471 return yield* handler(request)
1472 }),
1473 Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
1474 )
1475
1476const sseResponse = (
1477 request: HttpClientRequest.HttpClientRequest,
1478 events: ReadonlyArray<unknown>
1479): HttpClientResponse.HttpClientResponse =>
1480 HttpClientResponse.fromWeb(
1481 request,
1482 new Response(toSseBody(events), {
1483 status: 200,
1484 headers: {
1485 "content-type": "text/event-stream"
1486 }
1487 })
1488 )
1489
1490const jsonResponse = (
1491 request: HttpClientRequest.HttpClientRequest,
1492 body: unknown
1493): HttpClientResponse.HttpClientResponse =>
1494 HttpClientResponse.fromWeb(
1495 request,
1496 new Response(JSON.stringify(body), {
1497 status: 200,
1498 headers: {
1499 "content-type": "application/json"
1500 }
1501 })
1502 )
1503
1504const getRequestBody = (request: HttpClientRequest.HttpClientRequest) =>
1505 Effect.gen(function*() {
1506 const body = request.body
1507 if (body._tag !== "Uint8Array") {
1508 return yield* Effect.die(new Error("Expected Uint8Array body"))
1509 }
1510 return JSON.parse(new TextDecoder().decode(body.body))
1511 })
1512
1513const toSseBody = (events: ReadonlyArray<unknown>): string =>
1514 events.map((event) => `event: message_stream\ndata: ${JSON.stringify(event)}\n\n`).join("")