1/**
2 * The `AnthropicLanguageModel` module provides the Anthropic implementation of
3 * Effect AI's `LanguageModel` service. It translates Effect AI prompts, tools,
4 * files, reasoning content, and Anthropic-specific options into Messages API
5 * requests, then converts normal and streaming Anthropic responses back into
6 * Effect AI response content with provider metadata.
7 *
8 * @stability unstable
9 * @since 4.0.0
10 */
11/** @effect-diagnostics preferSchemaOverJson:skip-file */
12import * as AiError from "effect/ai/AiError"
13import { toCodecAnthropic } from "effect/ai/AnthropicStructuredOutput"
14import * as IdGenerator from "effect/ai/IdGenerator"
15import * as LanguageModel from "effect/ai/LanguageModel"
16import * as AiModel from "effect/ai/Model"
17import type * as Prompt from "effect/ai/Prompt"
18import type * as Response from "effect/ai/Response"
19import * as Tool from "effect/ai/Tool"
20import * as Arr from "effect/Array"
21import * as Context from "effect/Context"
22import * as DateTime from "effect/DateTime"
23import * as Effect from "effect/Effect"
24import * as Base64 from "effect/encoding/Base64"
25import { dual } from "effect/Function"
26import type * as HttpClientRequest from "effect/http/HttpClientRequest"
27import type * as HttpClientResponse from "effect/http/HttpClientResponse"
28import * as Layer from "effect/Layer"
29import * as Option from "effect/Option"
30import * as Predicate from "effect/Predicate"
31import * as Redactable from "effect/Redactable"
32import * as Schema from "effect/Schema"
33import * as SchemaAST from "effect/SchemaAST"
34import * as Stream from "effect/Stream"
35import type { Span } from "effect/Tracer"
36import type { Mutable, Simplify } from "effect/Types"
37import { AnthropicClient, type MessageStreamEvent } from "./AnthropicClient.ts"
38import { addGenAIAnnotations } from "./AnthropicTelemetry.ts"
39import type { AnthropicTool } from "./AnthropicTool.ts"
40import type * as Generated from "./Generated.ts"
41import * as InternalUtilities from "./internal/utilities.ts"
42
43/**
44 * Known Anthropic Claude model identifiers exposed by the generated Anthropic schema.
45 *
46 * @stability unstable
47 * @category models
48 * @since 4.0.0
49 */
50export type Model = (typeof Generated.Model)["members"][1]["Encoded"]
51
52// =============================================================================
53// Configuration
54// =============================================================================
55
56/**
57 * Context service for Anthropic language model configuration.
58 *
59 * **When to use**
60 *
61 * Use when you need scoped Anthropic model request defaults or per-operation
62 * overrides from Effect context.
63 *
64 * **Details**
65 *
66 * The service stores request fields that are merged into Anthropic Messages API
67 * requests. Scoped configuration overrides defaults supplied to `model`,
68 * `make`, or `layer`.
69 *
70 * @stability unstable
71 * @category services
72 * @since 4.0.0
73 */
74export class Config extends Context.Service<
75 Config,
76 Simplify<
77 & Partial<
78 Omit<
79 typeof Generated.BetaCreateMessageParams.Encoded,
80 "messages" | "output_config" | "tools" | "tool_choice" | "stream"
81 >
82 >
83 & {
84 readonly output_config?: {
85 readonly effort?: "low" | "medium" | "high" | null
86 }
87 /**
88 * Disables Claude's ability to use multiple tools to respond to a query.
89 */
90 readonly disableParallelToolCalls?: boolean | undefined
91 /**
92 * Whether the model supports native structured outputs.
93 *
94 * Overrides automatic capability detection based on the model identifier.
95 */
96 readonly structuredOutputs?: boolean | undefined
97 /**
98 * Overrides model detection for mid-conversation system messages.
99 * Set to `false` to send all instructions in the top-level `system` field.
100 *
101 * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
102 */
103 readonly midConversationSystemMessages?: boolean | undefined
104 /**
105 * Whether to use strict JSON schema validation for tool calls.
106 *
107 * **Details**
108 *
109 * Only applies to models that support structured outputs. Defaults to
110 * `true` when structured outputs are supported.
111 */
112 readonly strictJsonSchema?: boolean | undefined
113 }
114 >
115>()("@effect/ai-anthropic/AnthropicLanguageModel/Config") {}
116
117// =============================================================================
118// Provider Options / Metadata
119// =============================================================================
120
121declare module "effect/ai/Prompt" {
122 /**
123 * Anthropic-specific options for system messages.
124 *
125 * **Details**
126 *
127 * These options are used when translating system messages into Anthropic
128 * request content.
129 *
130 * @stability unstable
131 * @category models
132 * @since 4.0.0
133 */
134 export interface SystemMessageOptions extends ProviderOptions {
135 readonly anthropic?: {
136 /**
137 * A breakpoint which marks the end of reusable content eligible for caching.
138 */
139 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
140 } | null
141 }
142
143 /**
144 * Anthropic-specific options for user messages.
145 *
146 * **Details**
147 *
148 * These options are used when translating user messages into Anthropic
149 * request content.
150 *
151 * @stability unstable
152 * @category models
153 * @since 4.0.0
154 */
155 export interface UserMessageOptions extends ProviderOptions {
156 readonly anthropic?: {
157 /**
158 * A breakpoint which marks the end of reusable content eligible for caching.
159 */
160 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
161 } | null
162 }
163
164 /**
165 * Anthropic-specific options for assistant messages.
166 *
167 * **Details**
168 *
169 * These options are used when replaying assistant messages in Anthropic
170 * conversation history.
171 *
172 * @stability unstable
173 * @category models
174 * @since 4.0.0
175 */
176 export interface AssistantMessageOptions extends ProviderOptions {
177 readonly anthropic?: {
178 /**
179 * A breakpoint which marks the end of reusable content eligible for caching.
180 */
181 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
182 } | null
183 }
184
185 /**
186 * Anthropic-specific options for tool messages.
187 *
188 * **Details**
189 *
190 * These options are used when converting tool results into Anthropic user
191 * content blocks.
192 *
193 * @stability unstable
194 * @category models
195 * @since 4.0.0
196 */
197 export interface ToolMessageOptions extends ProviderOptions {
198 readonly anthropic?: {
199 /**
200 * A breakpoint which marks the end of reusable content eligible for caching.
201 */
202 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
203 } | null
204 }
205
206 /**
207 * Anthropic-specific options for text prompt parts.
208 *
209 * **When to use**
210 *
211 * Use when you use these options to control how text blocks are sent to Anthropic.
212 *
213 * @stability unstable
214 * @category models
215 * @since 4.0.0
216 */
217 export interface TextPartOptions extends ProviderOptions {
218 readonly anthropic?: {
219 /**
220 * A breakpoint which marks the end of reusable content eligible for caching.
221 */
222 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
223 } | null
224 }
225
226 /**
227 * Anthropic-specific options for reasoning prompt parts.
228 *
229 * **Details**
230 *
231 * Preserves Claude thinking metadata when reasoning content is sent back to
232 * Anthropic in later turns.
233 *
234 * @stability unstable
235 * @category models
236 * @since 4.0.0
237 */
238 export interface ReasoningPartOptions extends ProviderOptions {
239 readonly anthropic?: {
240 readonly info?: {
241 readonly type: "thinking"
242 /**
243 * Thinking content as an encrypted string, which is used to verify
244 * that thinking content was indeed generated by Anthropic's API.
245 */
246 readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded
247 } | {
248 readonly type: "redacted_thinking"
249 /**
250 * Thinking content which was flagged by Anthropic's safety systems, and
251 * was therefore encrypted.
252 */
253 readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded
254 } | null
255 /**
256 * A breakpoint which marks the end of reusable content eligible for caching.
257 */
258 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
259 } | null
260 }
261
262 /**
263 * Anthropic-specific options for file prompt parts.
264 *
265 * **Details**
266 *
267 * Controls document metadata, citations, and prompt caching for files sent to
268 * Anthropic.
269 *
270 * @stability unstable
271 * @category models
272 * @since 4.0.0
273 */
274 export interface FilePartOptions extends ProviderOptions {
275 readonly anthropic?: {
276 /**
277 * A breakpoint which marks the end of reusable content eligible for caching.
278 */
279 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
280 /**
281 * Whether or not citations should be enabled for the file part.
282 */
283 readonly citations?: typeof Generated.RequestCitationsConfig.Encoded | null
284 /**
285 * A custom title to provide to the document. If omitted, the file part's
286 * `fileName` property will be used.
287 */
288 readonly documentTitle?: string | null
289 /**
290 * Additional context about the document that will be forwarded to the
291 * large language model, but will not be used towards cited content.
292 *
293 * **When to use**
294 *
295 * Use when storing additional document metadata as text or stringified JSON.
296 */
297 readonly documentContext?: string | null
298 } | null
299 }
300
301 /**
302 * Anthropic-specific options for tool call prompt parts.
303 *
304 * **Details**
305 *
306 * Carries Anthropic tool caller metadata, MCP metadata, and cache control for
307 * tool use blocks.
308 *
309 * @stability unstable
310 * @category models
311 * @since 4.0.0
312 */
313 export interface ToolCallPartOptions extends ProviderOptions {
314 readonly anthropic?: {
315 readonly caller?: {
316 readonly type: string
317 readonly toolId?: string | null
318 } | null
319 /**
320 * Contains details about the MCP tool that was called.
321 */
322 readonly mcp_tool?: {
323 /**
324 * The name of the MCP server
325 */
326 readonly server: string
327 } | null
328 /**
329 * A breakpoint which marks the end of reusable content eligible for caching.
330 */
331 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
332 } | null
333 }
334
335 /**
336 * Anthropic-specific options for tool result prompt parts.
337 *
338 * **Details**
339 *
340 * Carries Anthropic MCP metadata and controls prompt caching for tool result
341 * content.
342 *
343 * @stability unstable
344 * @category models
345 * @since 4.0.0
346 */
347 export interface ToolResultPartOptions extends ProviderOptions {
348 readonly anthropic?: {
349 /**
350 * Contains details about the MCP tool that produced the result.
351 */
352 readonly mcp_tool?: {
353 /**
354 * The name of the MCP server
355 */
356 readonly server: string
357 } | null
358 /**
359 * A breakpoint which marks the end of reusable content eligible for caching.
360 */
361 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
362 } | null
363 }
364
365 /**
366 * Anthropic-specific options for tool approval request prompt parts.
367 *
368 * **Details**
369 *
370 * Controls prompt caching for human approval requests in conversations.
371 *
372 * @stability unstable
373 * @category models
374 * @since 4.0.0
375 */
376 export interface ToolApprovalRequestPartOptions extends ProviderOptions {
377 readonly anthropic?: {
378 /**
379 * A breakpoint which marks the end of reusable content eligible for caching.
380 */
381 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
382 } | null
383 }
384
385 /**
386 * Anthropic-specific options for tool approval response prompt parts.
387 *
388 * **Details**
389 *
390 * Controls prompt caching for human approval responses in conversations.
391 *
392 * @stability unstable
393 * @category models
394 * @since 4.0.0
395 */
396 export interface ToolApprovalResponsePartOptions extends ProviderOptions {
397 readonly anthropic?: {
398 /**
399 * A breakpoint which marks the end of reusable content eligible for caching.
400 */
401 readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | null
402 } | null
403 }
404}
405
406declare module "effect/ai/Response" {
407 /**
408 * Anthropic metadata attached when a reasoning block begins.
409 *
410 * **Details**
411 *
412 * Includes Claude thinking metadata needed to continue reasoning-aware
413 * conversations.
414 *
415 * @stability unstable
416 * @category models
417 * @since 4.0.0
418 */
419 export interface ReasoningStartPartMetadata extends ProviderMetadata {
420 readonly anthropic?: {
421 readonly info?: {
422 readonly type: "thinking"
423 /**
424 * Thinking content as an encrypted string, which is used to verify
425 * that thinking content was indeed generated by Anthropic's API.
426 */
427 readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded
428 } | {
429 readonly type: "redacted_thinking"
430 /**
431 * Thinking content which was flagged by Anthropic's safety systems, and
432 * was therefore encrypted.
433 */
434 readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded
435 } | null
436 } | null
437 }
438
439 /**
440 * Anthropic metadata attached to streaming reasoning deltas.
441 *
442 * **Details**
443 *
444 * Includes the signature for streamed Claude thinking content when available.
445 *
446 * @stability unstable
447 * @category models
448 * @since 4.0.0
449 */
450 export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
451 readonly anthropic?: {
452 readonly info?: {
453 readonly type: "thinking"
454 /**
455 * Thinking content as an encrypted string, which is used to verify
456 * that thinking content was indeed generated by Anthropic's API.
457 */
458 readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded
459 } | null
460 } | null
461 }
462
463 /**
464 * Anthropic metadata attached to completed reasoning parts.
465 *
466 * **Details**
467 *
468 * Preserves Claude thinking or redacted thinking information for later turns.
469 *
470 * @stability unstable
471 * @category models
472 * @since 4.0.0
473 */
474 export interface ReasoningPartMetadata extends ProviderMetadata {
475 readonly anthropic?: {
476 readonly info?: {
477 readonly type: "thinking"
478 /**
479 * Thinking content as an encrypted string, which is used to verify
480 * that thinking content was indeed generated by Anthropic's API.
481 */
482 readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded
483 } | {
484 readonly type: "redacted_thinking"
485 /**
486 * Thinking content which was flagged by Anthropic's safety systems, and
487 * was therefore encrypted.
488 */
489 readonly redactedData: typeof Generated.ResponseRedactedThinkingBlock.fields.data.Encoded
490 } | null
491 } | null
492 }
493
494 /**
495 * Anthropic metadata attached to tool call response parts.
496 *
497 * **Details**
498 *
499 * Identifies Anthropic caller details and MCP tool metadata emitted by the
500 * provider.
501 *
502 * @stability unstable
503 * @category models
504 * @since 4.0.0
505 */
506 export interface ToolCallPartMetadata extends ProviderMetadata {
507 readonly anthropic?: {
508 readonly caller?: {
509 readonly type: string
510 readonly toolId?: string | null
511 } | null
512 /**
513 * Contains details about the MCP tool that was called.
514 */
515 readonly mcp_tool?: {
516 /**
517 * The name of the MCP server
518 */
519 readonly server: string
520 } | null
521 } | null
522 }
523
524 /**
525 * Anthropic metadata attached to tool result response parts.
526 *
527 * **Details**
528 *
529 * Identifies MCP tool metadata associated with provider-executed tool
530 * results.
531 *
532 * @stability unstable
533 * @category models
534 * @since 4.0.0
535 */
536 export interface ToolResultPartMetadata extends ProviderMetadata {
537 readonly anthropic?: {
538 /**
539 * Contains details about the MCP tool that was called.
540 */
541 readonly mcp_tool?: {
542 /**
543 * The name of the MCP server
544 */
545 readonly server: string
546 } | null
547 } | null
548 }
549
550 /**
551 * Anthropic metadata for document citations in model responses.
552 *
553 * **Details**
554 *
555 * Records the cited document span by character position or page number.
556 *
557 * @stability unstable
558 * @category models
559 * @since 4.0.0
560 */
561 export interface DocumentSourcePartMetadata extends ProviderMetadata {
562 readonly anthropic?: {
563 readonly source: "document"
564 readonly type: "char_location"
565 /**
566 * The text that was cited in the response.
567 */
568 readonly citedText: string
569 /**
570 * The 0-indexed starting position of the characters that were cited.
571 */
572 readonly startCharIndex: number
573 /**
574 * The exclusive ending position of the characters that were cited.
575 */
576 readonly endCharIndex: number
577 } | {
578 readonly source: "document"
579 readonly type: "page_location"
580 /**
581 * The text that was cited in the response.
582 */
583 readonly citedText: string
584 /**
585 * The 1-indexed starting page of pages that were cited.
586 */
587 readonly startPageNumber: number
588 /**
589 * The exclusive ending position of the pages that were cited.
590 */
591 readonly endPageNumber: number
592 } | null
593 }
594
595 /**
596 * Anthropic metadata for URL and web citations in model responses.
597 *
598 * **Details**
599 *
600 * Records cited URL text or web-search source freshness information.
601 *
602 * @stability unstable
603 * @category models
604 * @since 4.0.0
605 */
606 export interface UrlSourcePartMetadata extends ProviderMetadata {
607 readonly anthropic?: {
608 readonly source: "url"
609 /**
610 * Up to 150 characters of the text content that was referenced from the
611 * URL source material.
612 */
613 readonly citedText: string
614 /**
615 * An internal reference that must be passed back to the Anthropic API
616 * during multi-turn conversations.
617 */
618 readonly encryptedIndex: string
619 } | {
620 readonly source: "web"
621 readonly pageAge: string | null
622 } | null
623 }
624
625 /**
626 * Anthropic metadata attached to the finish part of a response.
627 *
628 * **Details**
629 *
630 * Includes container state, context management information, stop details, and
631 * token usage reported by Anthropic.
632 *
633 * @stability unstable
634 * @category models
635 * @since 4.0.0
636 */
637 export interface FinishPartMetadata extends ProviderMetadata {
638 readonly anthropic?: {
639 readonly container: typeof Generated.BetaContainer.Encoded | null
640 readonly contextManagement: typeof Generated.BetaResponseContextManagement.Encoded | null
641 readonly stopSequence: string | null
642 readonly usage: typeof Generated.BetaMessage.Encoded["usage"] | null
643 } | null
644 }
645
646 /**
647 * Anthropic metadata attached to error response parts.
648 *
649 * **Details**
650 *
651 * Includes the provider request identifier when Anthropic returns one.
652 *
653 * @stability unstable
654 * @category models
655 * @since 4.0.0
656 */
657 export interface ErrorPartMetadata extends ProviderMetadata {
658 readonly anthropic?: {
659 requestId?: string | null
660 } | null
661 }
662}
663
664// =============================================================================
665// Language Model
666// =============================================================================
667
668/**
669 * Creates an Anthropic model descriptor that can be provided with `Effect.provide`.
670 *
671 * **When to use**
672 *
673 * Use when you want an Anthropic Claude model value that carries provider and
674 * model metadata and can be supplied directly to an Effect program.
675 *
676 * @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
677 * @see {@link make} for constructing the language model service effectfully
678 *
679 * @stability unstable
680 * @category constructors
681 * @since 4.0.0
682 */
683export const model = (
684 model: (string & {}) | Model,
685 config?: Omit<typeof Config.Service, "model">
686): AiModel.Model<"anthropic", LanguageModel.LanguageModel, AnthropicClient> =>
687 AiModel.make("anthropic", model, layer({ model, config }))
688
689/**
690 * Creates an Anthropic `LanguageModel` service from a model identifier and optional request defaults.
691 *
692 * **When to use**
693 *
694 * Use when you need to construct a `LanguageModel` value backed by
695 * `AnthropicClient` inside an Effect.
696 *
697 * **Details**
698 *
699 * The returned effect requires `AnthropicClient`. Request defaults from the
700 * `config` option are merged with any `Config` service in the context, with
701 * context values taking precedence.
702 *
703 * @see {@link layer} for providing the service as a `Layer`
704 * @see {@link model} for creating a model descriptor for `AiModel.provide`
705 *
706 * @stability unstable
707 * @category constructors
708 * @since 4.0.0
709 */
710export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
711 readonly model: (string & {}) | Model
712 readonly config?: Omit<typeof Config.Service, "model"> | undefined
713}): Effect.fn.Return<LanguageModel.LanguageModel, never, AnthropicClient> {
714 const client = yield* AnthropicClient
715
716 const makeConfig: Effect.Effect<typeof Config.Service & { readonly model: string }> = Effect.contextWith((services) =>
717 Effect.succeed({
718 model,
719 ...providerConfig,
720 ...Context.getOrUndefined(services, Config)
721 })
722 )
723
724 const makeRequest = Effect.fnUntraced(
725 function*<Tools extends ReadonlyArray<Tool.Any>>({ config, options, toolNameMapper }: {
726 readonly config: typeof Config.Service & { readonly model: string }
727 readonly options: LanguageModel.ProviderOptions
728 readonly toolNameMapper: Tool.NameMapper<Tools>
729 }): Effect.fn.Return<{
730 readonly params: typeof Generated.BetaMessagesPostParams.Encoded
731 readonly payload: typeof Generated.BetaCreateMessageParams.Encoded
732 }, AiError.AiError> {
733 const betas = new Set<string>()
734 const capabilities = getConfigCapabilities(config)
735 const { messages, system } = yield* prepareMessages({ betas, capabilities, options, toolNameMapper })
736 const outputFormat = yield* getOutputFormat({ capabilities, options })
737 const { tools, toolChoice } = yield* prepareTools({ betas, capabilities, config, options })
738 const params: Mutable<typeof Generated.BetaMessagesPostParams.Encoded> = {}
739 if (betas.size > 0) {
740 params["anthropic-beta"] = Array.from(betas).join(",")
741 }
742 const {
743 disableParallelToolCalls: _,
744 output_config,
745 strictJsonSchema: _strictJsonSchema,
746 structuredOutputs: _structuredOutputs,
747 midConversationSystemMessages: _midConversationSystemMessages,
748 ...requestConfig
749 } = config
750 const payload: Mutable<typeof Generated.BetaCreateMessageParams.Encoded> = {
751 ...requestConfig,
752 max_tokens: requestConfig.max_tokens ?? capabilities.maxOutputTokens,
753 messages,
754 ...(Predicate.isNotUndefined(system) ? { system } : undefined),
755 ...(Predicate.isNotUndefined(tools) ? { tools } : undefined),
756 ...(Predicate.isNotUndefined(toolChoice) ? { tool_choice: toolChoice } : undefined)
757 }
758 const outputConfig: Mutable<typeof Generated.BetaCreateMessageParams.Encoded["output_config"]> = {}
759 if (Predicate.isNotUndefined(outputFormat)) {
760 outputConfig.format = outputFormat
761 }
762 if (Predicate.isNotUndefined(output_config?.effort)) {
763 outputConfig.effort = output_config.effort
764 }
765 if (Object.keys(outputConfig).length > 0) {
766 payload.output_config = outputConfig
767 }
768 return { params, payload }
769 }
770 )
771
772 return yield* LanguageModel.make({
773 codecTransformer: toCodecAnthropic,
774 supportsSystemMessagesInHistory: Effect.map(
775 makeConfig,
776 (config) => getConfigCapabilities(config).supportsMidConversationSystemMessages
777 ),
778 generateText: Effect.fnUntraced(function*(options) {
779 const config = yield* makeConfig
780 const toolNameMapper = new Tool.NameMapper(options.tools)
781 const request = yield* makeRequest({ config, options, toolNameMapper })
782 annotateRequest(options.span, request.payload)
783 const [rawResponse, response] = yield* client.createMessage(request)
784 annotateResponse(options.span, rawResponse)
785 return yield* makeResponse({ options, rawResponse, response, toolNameMapper })
786 }),
787 streamText: Effect.fnUntraced(function*(options) {
788 const config = yield* makeConfig
789 const toolNameMapper = new Tool.NameMapper(options.tools)
790 const request = yield* makeRequest({ config, options, toolNameMapper })
791 annotateRequest(options.span, request.payload)
792 const [response, stream] = yield* client.createMessageStream(request)
793 return yield* makeStreamResponse({ stream, response, options, toolNameMapper })
794 }, (effect, options) =>
795 effect.pipe(
796 Stream.unwrap,
797 Stream.map((response) => {
798 annotateStreamResponse(options.span, response)
799 return response
800 })
801 ))
802 })
803})
804
805/**
806 * Creates a layer for the Anthropic language model.
807 *
808 * **When to use**
809 *
810 * Use when composing application layers and you want Anthropic to satisfy
811 * `LanguageModel.LanguageModel` while supplying `AnthropicClient` from another
812 * layer.
813 *
814 * @see {@link make} for constructing the language model service effectfully
815 * @see {@link model} for creating a model service directly
816 *
817 * @stability unstable
818 * @category layers
819 * @since 4.0.0
820 */
821export const layer = (options: {
822 readonly model: (string & {}) | Model
823 readonly config?: Omit<typeof Config.Service, "model"> | undefined
824}): Layer.Layer<LanguageModel.LanguageModel, never, AnthropicClient> =>
825 Layer.effect(LanguageModel.LanguageModel, make(options))
826
827/**
828 * Provides config overrides for Anthropic language model operations.
829 *
830 * **When to use**
831 *
832 * Use to apply Anthropic request configuration to one effect without changing
833 * the model's default configuration.
834 *
835 * **Details**
836 *
837 * The overrides are merged with any existing `Config` service for the duration
838 * of the supplied effect. Fields in `overrides` take precedence over existing
839 * config, and the helper supports both `effect.pipe(withConfigOverride(overrides))`
840 * and `withConfigOverride(effect, overrides)`.
841 *
842 * @see {@link Config} for available Anthropic request configuration fields
843 *
844 * @stability unstable
845 * @category configuration
846 * @since 4.0.0
847 */
848export const withConfigOverride: {
849 (overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
850 <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
851} = dual<
852 (
853 overrides: typeof Config.Service
854 ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
855 <A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
856>(2, (self, overrides) =>
857 Effect.flatMap(
858 Effect.serviceOption(Config),
859 (config) =>
860 Effect.provideService(self, Config, {
861 ...(config._tag === "Some" ? config.value : {}),
862 ...overrides
863 })
864 ))
865
866// =============================================================================
867// Prompt Conversion
868// =============================================================================
869
870const prepareMessages = Effect.fnUntraced(
871 function*<Tools extends ReadonlyArray<Tool.Any>>({ betas, capabilities, options, toolNameMapper }: {
872 readonly betas: Set<string>
873 readonly capabilities: ModelCapabilities
874 readonly options: LanguageModel.ProviderOptions
875 readonly toolNameMapper: Tool.NameMapper<Tools>
876 }): Effect.fn.Return<{
877 readonly system: ReadonlyArray<typeof Generated.BetaRequestTextBlock.Encoded> | undefined
878 readonly messages: ReadonlyArray<typeof Generated.BetaInputMessage.Encoded>
879 }, AiError.AiError> {
880 const groups = groupMessages(options.prompt)
881
882 // A system message in history must directly follow a user turn and directly
883 // precede an assistant turn or the end of the prompt, so later instructions
884 // are held until the next assistant turn. If any of them cannot be placed
885 // that way, every instruction goes in the top-level `system` field instead:
886 // inline instructions override top-level ones, so mixing would reorder them.
887 const inlineSystem = capabilities.supportsMidConversationSystemMessages &&
888 groups.every((group, i) =>
889 i === 0 || group.type !== "system" || groups[i - 1].type === "user" || groups[i + 1]?.type === "user"
890 )
891
892 const system: Array<typeof Generated.BetaRequestTextBlock.Encoded> = []
893 const messages: Array<typeof Generated.BetaInputMessage.Encoded> = []
894 let pendingSystem: Array<typeof Generated.BetaRequestTextBlock.Encoded> = []
895
896 for (let i = 0; i < groups.length; i++) {
897 const group = groups[i]
898 const isLastGroup = i === groups.length - 1
899
900 switch (group.type) {
901 case "system": {
902 const target = i === 0 || !inlineSystem ? system : pendingSystem
903 for (const message of group.messages) {
904 target.push({
905 type: "text",
906 text: message.content,
907 cache_control: getCacheControl(message)
908 })
909 }
910 break
911 }
912
913 case "user": {
914 const content: Array<typeof Generated.BetaInputContentBlock.Encoded> = []
915
916 for (const message of group.messages) {
917 switch (message.role) {
918 case "user": {
919 for (let j = 0; j < message.content.length; j++) {
920 const part = message.content[j]
921 const isLastPart = j === message.content.length - 1
922
923 // Attempt to get the cache control from the part first. If
924 // the part does not have cache control defined and we are
925 // evaluating the last part for this message, also check the
926 // message for cache control.
927 const cacheControl = getCacheControl(part) ?? (
928 isLastPart ? getCacheControl(message) : null
929 )
930
931 switch (part.type) {
932 case "text": {
933 content.push({
934 type: "text",
935 text: part.text,
936 cache_control: cacheControl
937 })
938 break
939 }
940
941 case "file": {
942 if (part.mediaType.startsWith("image/")) {
943 const mediaType: typeof Generated.Base64ImageSource.Type["media_type"] =
944 (part.mediaType === "image/*" ? "image/jpeg" : part.mediaType) as any
945
946 const source = isUrlData(part.data)
947 ? { type: "url", url: getUrlString(part.data) } as const
948 : {
949 type: "base64",
950 media_type: mediaType,
951 data: typeof part.data === "string"
952 ? part.data.replace(/^data:[^;]+;base64,/, "")
953 : Base64.encode(part.data)
954 } as const
955
956 content.push({ type: "image", source, cache_control: cacheControl })
957 } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") {
958 betas.add("pdfs-2024-09-25")
959
960 const enableCitations = areCitationsEnabled(part)
961 const documentOptions = getDocumentMetadata(part)
962
963 const source = isUrlData(part.data)
964 ? {
965 type: "url",
966 url: getUrlString(part.data)
967 } as const
968 : part.mediaType === "application/pdf"
969 ? {
970 type: "base64",
971 media_type: "application/pdf",
972 data: typeof part.data === "string" ? part.data : Base64.encode(part.data)
973 } as const
974 : {
975 type: "text",
976 media_type: "text/plain",
977 data: typeof part.data === "string" ? part.data : new TextDecoder().decode(part.data)
978 } as const
979
980 content.push({
981 type: "document",
982 source,
983 title: documentOptions?.title ?? part.fileName ?? null,
984 cache_control: cacheControl,
985 ...(documentOptions?.context ? { context: documentOptions.context } : undefined),
986 ...(enableCitations ? { citations: { enabled: true } } : undefined)
987 })
988 } else {
989 return yield* new AiError.AiError({
990 module: "AnthropicLanguageModel",
991 method: "prepareMessages",
992 reason: new AiError.InvalidUserInputError({
993 description: `Detected unsupported media type for file: '${part.mediaType}'`
994 })
995 })
996 }
997
998 break
999 }
1000 }
1001 }
1002 break
1003 }
1004
1005 case "tool": {
1006 for (let j = 0; j < message.content.length; j++) {
1007 const part = message.content[j]
1008
1009 // Skip evaluation of tool approval parts
1010 if (part.type === "tool-approval-response") {
1011 continue
1012 }
1013
1014 const isLastPart = j === message.content.length - 1
1015
1016 // Attempt to get the cache control from the part first. If
1017 // the part does not have cache control defined and we are
1018 // evaluating the last part for this message, also check the
1019 // message for cache control.
1020 const cacheControl = getCacheControl(part) ?? (
1021 isLastPart ? getCacheControl(message) : null
1022 )
1023
1024 content.push({
1025 type: "tool_result",
1026 tool_use_id: part.id,
1027 content: typeof part.result === "string" ? part.result : JSON.stringify(part.result),
1028 is_error: part.isFailure,
1029 cache_control: cacheControl
1030 })
1031 }
1032 }
1033 }
1034 }
1035
1036 messages.push({ role: "user", content })
1037
1038 break
1039 }
1040
1041 case "assistant": {
1042 if (pendingSystem.length > 0) {
1043 messages.push({ role: "system", content: pendingSystem })
1044 pendingSystem = []
1045 }
1046
1047 const content: Array<typeof Generated.BetaContentBlock.Encoded> = []
1048 const mcpToolIds = new Set<string>()
1049
1050 for (let j = 0; j < group.messages.length; j++) {
1051 const message = group.messages[j]
1052 const isLastMessage = j === group.messages.length - 1
1053
1054 for (let k = 0; k < message.content.length; k++) {
1055 const part = message.content[k]
1056
1057 if (part.type === "file" || part.type === "tool-approval-request") {
1058 continue
1059 }
1060
1061 const isLastPart = k === message.content.length - 1
1062
1063 // Attempt to get the cache control from the part first. If
1064 // the part does not have cache control defined and we are
1065 // evaluating the last part for this message, also check the
1066 // message for cache control.
1067 const cacheControl = getCacheControl(part) ?? (
1068 isLastPart ? getCacheControl(message) : undefined
1069 )
1070
1071 // TODO: use cache_control in content blocks
1072 void cacheControl
1073
1074 switch (part.type) {
1075 case "text": {
1076 content.push({
1077 type: "text",
1078 // Anthropic does not allow trailing whitespace in assistant
1079 // content blocks
1080 text: isLastGroup && isLastMessage && isLastPart
1081 ? part.text.trim()
1082 : part.text
1083 })
1084 break
1085 }
1086 case "reasoning": {
1087 // TODO: make sending reasoning configurable
1088 const info = part.options.anthropic?.info
1089 if (Predicate.isNotNullish(info)) {
1090 if (info.type === "thinking") {
1091 content.push({
1092 type: "thinking",
1093 thinking: part.text,
1094 signature: info.signature
1095 })
1096 } else {
1097 content.push({
1098 type: "redacted_thinking",
1099 data: info.redactedData
1100 })
1101 }
1102 }
1103 break
1104 }
1105
1106 case "tool-call": {
1107 if (part.providerExecuted) {
1108 const toolName = toolNameMapper.getProviderName(part.name)
1109
1110 const isMcpTool = Predicate.isNotNullish(part.options.anthropic?.mcp_tool)
1111
1112 if (isMcpTool) {
1113 const { server } = part.options.anthropic.mcp_tool
1114
1115 mcpToolIds.add(part.id)
1116
1117 content.push({
1118 type: "mcp_tool_use",
1119 id: part.id,
1120 name: part.name,
1121 input: part.params as any,
1122 server_name: server
1123 })
1124 } else if (
1125 toolName === "code_execution" &&
1126 Predicate.hasProperty(part.params, "type") &&
1127 (
1128 part.params.type === "bash_code_execution" ||
1129 part.params.type === "text_editor_code_execution"
1130 )
1131 ) {
1132 content.push({
1133 type: "server_tool_use",
1134 id: part.id,
1135 name: part.params.type,
1136 input: part.params as any
1137 })
1138 } else if (
1139 // code execution 20250825 programmatic tool calling:
1140 // Strip the fake 'programmatic-tool-call' type before sending to Anthropic
1141 toolName === "code_execution" &&
1142 Predicate.hasProperty(part.params, "type") &&
1143 part.params.type === "programmatic-tool-call"
1144 ) {
1145 const { type, ...params } = part.params
1146 content.push({
1147 type: "server_tool_use",
1148 id: part.id,
1149 name: toolName,
1150 input: params as any
1151 })
1152 } else {
1153 if (
1154 // code execution 20250522
1155 toolName === "code_execution" ||
1156 toolName === "tool_search_tool_regex" ||
1157 toolName === "tool_search_tool_bm25" ||
1158 toolName === "web_fetch" ||
1159 toolName === "web_search"
1160 ) {
1161 content.push({
1162 type: "server_tool_use",
1163 id: part.id,
1164 name: toolName,
1165 input: part.params as any
1166 })
1167 }
1168 }
1169 } else {
1170 // Extract caller info from provider options for programmatic tool calling
1171 const options = part.options.anthropic
1172 const caller = Predicate.isNotNullish(options?.caller)
1173 ? (
1174 options.caller.type === "code_execution_20250825" &&
1175 Predicate.isNotNullish(options.caller.toolId)
1176 )
1177 ? {
1178 type: "code_execution_20250825",
1179 tool_id: options.caller.toolId
1180 } as const
1181 : options.caller.type === "direct"
1182 ? {
1183 type: "direct"
1184 } as const
1185 : undefined
1186 : undefined
1187
1188 content.push({
1189 type: "tool_use",
1190 id: part.id,
1191 name: part.name,
1192 input: part.params as any,
1193 ...(Predicate.isNotUndefined(caller) ? { caller } : undefined)
1194 })
1195 }
1196
1197 break
1198 }
1199
1200 case "tool-result": {
1201 const toolName = toolNameMapper.getProviderName(part.name)
1202
1203 if (mcpToolIds.has(part.id)) {
1204 content.push({
1205 type: "mcp_tool_result",
1206 tool_use_id: part.id,
1207 is_error: part.isFailure,
1208 content: part.result as any
1209 })
1210 break
1211 }
1212
1213 if (toolName === "code_execution" && Predicate.hasProperty(part.result, "type")) {
1214 if (part.result.type === "code_execution_result") {
1215 content.push({
1216 type: "code_execution_tool_result",
1217 tool_use_id: part.id,
1218 content: part.result as any
1219 })
1220 } else if (
1221 part.result.type === "bash_code_execution_result" ||
1222 part.result.type === "bash_code_execution_tool_result_error"
1223 ) {
1224 content.push({
1225 type: "bash_code_execution_tool_result",
1226 tool_use_id: part.id,
1227 content: part.result as any
1228 })
1229 } else if (
1230 part.result.type === "text_editor_code_execution_tool_result" ||
1231 part.result.type === "text_editor_code_execution_tool_result_error"
1232 ) {
1233 content.push({
1234 type: "text_editor_code_execution_tool_result",
1235 tool_use_id: part.id,
1236 content: part.result as any
1237 })
1238 }
1239 break
1240 }
1241
1242 if (toolName === "web_fetch") {
1243 content.push({
1244 type: "web_fetch_tool_result",
1245 tool_use_id: part.id,
1246 content: part.result as any
1247 })
1248 break
1249 }
1250
1251 if (toolName === "web_search") {
1252 content.push({
1253 type: "web_search_tool_result",
1254 tool_use_id: part.id,
1255 content: part.result as any
1256 })
1257 break
1258 }
1259
1260 if (
1261 toolName === "tool_search_tool_regex" ||
1262 toolName === "tool_search_tool_bm25"
1263 ) {
1264 content.push({
1265 type: "tool_search_tool_result",
1266 tool_use_id: part.id,
1267 content: part.result as any
1268 })
1269 break
1270 }
1271
1272 break
1273 }
1274 }
1275 }
1276 }
1277
1278 messages.push({ role: "assistant", content })
1279
1280 break
1281 }
1282 }
1283 }
1284
1285 if (pendingSystem.length > 0) {
1286 messages.push({ role: "system", content: pendingSystem })
1287 }
1288
1289 return {
1290 system: system.length > 0 ? system : undefined,
1291 messages
1292 }
1293 }
1294)
1295
1296// =============================================================================
1297// Tool Conversion
1298// =============================================================================
1299
1300/**
1301 * Encoded Anthropic custom tool definition that can be sent in a Messages API request.
1302 *
1303 * **When to use**
1304 *
1305 * Use when you need to type or inspect the provider-specific request payload for
1306 * a custom Anthropic tool.
1307 *
1308 * **Details**
1309 *
1310 * This type aliases the encoded `Generated.BetaTool` schema used for Effect
1311 * user-defined and dynamic tools after conversion. It contains the tool `name`,
1312 * optional `description`, and `input_schema`, plus Anthropic-specific fields
1313 * such as `strict` and `cache_control`.
1314 *
1315 * @see {@link AnthropicProviderDefinedTool} for the request shape used by Anthropic built-in provider tools
1316 *
1317 * @stability unstable
1318 * @category tools
1319 * @since 4.0.0
1320 */
1321export type AnthropicUserDefinedTool = typeof Generated.BetaTool.Encoded
1322
1323/**
1324 * Represents a provider-defined tool that can be passed to the Anthropic API.
1325 *
1326 * **Details**
1327 *
1328 * These include Anthropic's built-in tools like computer use, code execution,
1329 * web search, and text editing.
1330 *
1331 * @stability unstable
1332 * @category tools
1333 * @since 4.0.0
1334 */
1335export type AnthropicProviderDefinedTool =
1336 | typeof Generated.BetaBashTool_20241022.Encoded
1337 | typeof Generated.BetaBashTool_20250124.Encoded
1338 | typeof Generated.BetaCodeExecutionTool_20250522.Encoded
1339 | typeof Generated.BetaCodeExecutionTool_20250825.Encoded
1340 | typeof Generated.BetaComputerUseTool_20241022.Encoded
1341 | typeof Generated.BetaComputerUseTool_20250124.Encoded
1342 | typeof Generated.BetaComputerUseTool_20251124.Encoded
1343 | typeof Generated.BetaMemoryTool_20250818.Encoded
1344 | typeof Generated.BetaTextEditor_20241022.Encoded
1345 | typeof Generated.BetaTextEditor_20250124.Encoded
1346 | typeof Generated.BetaTextEditor_20250429.Encoded
1347 | typeof Generated.BetaTextEditor_20250728.Encoded
1348 | typeof Generated.BetaToolSearchToolBM25_20251119.Encoded
1349 | typeof Generated.BetaToolSearchToolRegex_20251119.Encoded
1350 | typeof Generated.BetaWebFetchTool_20250910.Encoded
1351 | typeof Generated.BetaWebSearchTool_20250305.Encoded
1352
1353const prepareTools = Effect.fnUntraced(
1354 function*({ betas, capabilities, config, options }: {
1355 readonly betas: Set<string>
1356 readonly capabilities: ModelCapabilities
1357 readonly config: typeof Config.Service
1358 readonly options: LanguageModel.ProviderOptions
1359 }): Effect.fn.Return<{
1360 readonly tools: ReadonlyArray<AnthropicUserDefinedTool | AnthropicProviderDefinedTool> | undefined
1361 readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined
1362 }, AiError.AiError> {
1363 // Return a JSON response tool when using non-native structured outputs
1364 if (options.responseFormat.type === "json" && !capabilities.supportsStructuredOutput) {
1365 const input_schema = yield* tryJsonSchema(options.responseFormat.schema, "prepareTools")
1366 const userDescription = SchemaAST.resolveDescription(options.responseFormat.schema.ast)
1367 const description = Predicate.isNotUndefined(userDescription) ? `${userDescription} - ` : ""
1368 return {
1369 tools: [{
1370 name: options.responseFormat.objectName,
1371 description: `${description}You MUST respond with a JSON object.`,
1372 input_schema: input_schema as any
1373 }],
1374 toolChoice: {
1375 type: "tool",
1376 name: options.responseFormat.objectName,
1377 disable_parallel_tool_use: true
1378 }
1379 }
1380 }
1381
1382 if (options.tools.length === 0 || options.toolChoice === "none") {
1383 return { tools: undefined, toolChoice: undefined }
1384 }
1385
1386 const userTools: Array<AnthropicUserDefinedTool> = []
1387 const providerTools: Array<AnthropicProviderDefinedTool> = []
1388
1389 for (const tool of options.tools) {
1390 if (Tool.isUserDefined(tool) || Tool.isDynamic(tool)) {
1391 const description = Tool.getDescription(tool)
1392 const input_schema = yield* tryToolJsonSchema(tool, "prepareTools")
1393 const toolStrict = Tool.getStrictMode(tool)
1394 const strict = capabilities.supportsStructuredOutput
1395 ? (toolStrict ?? config.strictJsonSchema ?? true)
1396 : undefined
1397
1398 userTools.push({
1399 name: tool.name,
1400 input_schema: input_schema as any,
1401 ...(Predicate.isNotUndefined(description) ? { description } : undefined),
1402 ...(Predicate.isNotUndefined(strict) ? { strict } : undefined)
1403 })
1404
1405 if (capabilities.supportsStructuredOutput === true) {
1406 betas.add("structured-outputs-2025-11-13")
1407 }
1408 }
1409
1410 if (Tool.isProviderDefined(tool)) {
1411 const providerTool = tool as AnthropicTool
1412 switch (providerTool.id) {
1413 case "anthropic.bash_20241022": {
1414 betas.add("computer-use-2024-10-22")
1415 providerTools.push({ name: "bash", type: "bash_20241022" })
1416 break
1417 }
1418
1419 case "anthropic.bash_20250124": {
1420 betas.add("computer-use-2025-01-24")
1421 providerTools.push({ name: "bash", type: "bash_20250124" })
1422 break
1423 }
1424
1425 case "anthropic.code_execution_20250522": {
1426 betas.add("code-execution-2025-05-22")
1427 providerTools.push({ name: "code_execution", type: "code_execution_20250522" })
1428 break
1429 }
1430
1431 case "anthropic.code_execution_20250825": {
1432 betas.add("code-execution-2025-08-25")
1433 providerTools.push({ name: "code_execution", type: "code_execution_20250825" })
1434 break
1435 }
1436
1437 case "anthropic.computer_use_20241022": {
1438 betas.add("computer-use-2024-10-22")
1439 providerTools.push({
1440 name: "computer",
1441 type: "computer_20241022",
1442 display_height_px: providerTool.args.displayHeightPx,
1443 display_width_px: providerTool.args.displayWidthPx,
1444 display_number: providerTool.args.displayNumber ?? null
1445 })
1446 break
1447 }
1448
1449 case "anthropic.computer_20250124": {
1450 betas.add("computer-use-2025-01-24")
1451 providerTools.push({
1452 name: "computer",
1453 type: "computer_20250124",
1454 display_height_px: providerTool.args.displayHeightPx,
1455 display_width_px: providerTool.args.displayWidthPx,
1456 display_number: providerTool.args.displayNumber ?? null
1457 })
1458 break
1459 }
1460
1461 case "anthropic.computer_20251124": {
1462 betas.add("computer-use-2025-11-24")
1463 providerTools.push({
1464 name: "computer",
1465 type: "computer_20251124",
1466 display_height_px: providerTool.args.displayHeightPx,
1467 display_width_px: providerTool.args.displayWidthPx,
1468 display_number: providerTool.args.displayNumber ?? null,
1469 enable_zoom: providerTool.args.enableZoom ?? false
1470 })
1471 break
1472 }
1473
1474 case "anthropic.memory_20250818": {
1475 betas.add("context-management-2025-06-27")
1476 providerTools.push({ name: "memory", type: "memory_20250818" })
1477 break
1478 }
1479
1480 case "anthropic.text_editor_20241022": {
1481 betas.add("computer-use-2024-10-22")
1482 providerTools.push({ name: "str_replace_editor", type: "text_editor_20241022" })
1483 break
1484 }
1485
1486 case "anthropic.text_editor_20250124": {
1487 betas.add("computer-use-2025-01-24")
1488 providerTools.push({ name: "str_replace_editor", type: "text_editor_20250124" })
1489 break
1490 }
1491
1492 case "anthropic.text_editor_20250429": {
1493 betas.add("computer-use-2025-01-24")
1494 providerTools.push({ name: "str_replace_based_edit_tool", type: "text_editor_20250429" })
1495 break
1496 }
1497
1498 case "anthropic.text_editor_20250728": {
1499 providerTools.push({
1500 name: "str_replace_based_edit_tool",
1501 type: "text_editor_20250728",
1502 max_characters: providerTool.args.max_characters ?? null
1503 })
1504 break
1505 }
1506
1507 case "anthropic.tool_search_tool_bm25_20251119": {
1508 betas.add("advanced-tool-use-2025-11-20")
1509 providerTools.push({ name: "tool_search_tool_bm25", type: "tool_search_tool_bm25_20251119" })
1510 break
1511 }
1512
1513 case "anthropic.tool_search_tool_regex_20251119": {
1514 providerTools.push({ name: "tool_search_tool_regex", type: "tool_search_tool_regex_20251119" })
1515 break
1516 }
1517
1518 case "anthropic.web_search_20250305": {
1519 providerTools.push({
1520 name: "web_search",
1521 type: "web_search_20250305",
1522 max_uses: providerTool.args.maxUses ?? null,
1523 allowed_domains: providerTool.args.allowedDomains ?? null,
1524 blocked_domains: providerTool.args.blockedDomains ?? null,
1525 user_location: Predicate.isNotUndefined(providerTool.args.userLocation)
1526 ? {
1527 type: providerTool.args.userLocation.type,
1528 region: providerTool.args.userLocation.region ?? null,
1529 city: providerTool.args.userLocation.city ?? null,
1530 country: providerTool.args.userLocation.country ?? null,
1531 timezone: providerTool.args.userLocation.timezone ?? null
1532 }
1533 : null
1534 })
1535 break
1536 }
1537
1538 case "anthropic.web_fetch_20250910": {
1539 betas.add("web-fetch-2025-09-10")
1540 providerTools.push({
1541 name: "web_fetch",
1542 type: "web_fetch_20250910",
1543 max_uses: providerTool.args.maxUses ?? null,
1544 allowed_domains: providerTool.args.allowedDomains ?? null,
1545 blocked_domains: providerTool.args.blockedDomains ?? null,
1546 citations: providerTool.args.citations ?? null,
1547 max_content_tokens: providerTool.args.maxContentTokens ?? null
1548 })
1549 break
1550 }
1551
1552 default: {
1553 return yield* AiError.make({
1554 module: "AnthropicLanguageModel",
1555 method: "prepareTools",
1556 reason: new AiError.InvalidUserInputError({
1557 description: `Received request to call unknown provider-defined tool '${tool.name}'`
1558 })
1559 })
1560 }
1561 }
1562 }
1563 }
1564
1565 let tools = [...userTools, ...providerTools]
1566 let toolChoice: Mutable<typeof Generated.BetaToolChoice.Encoded> | undefined = undefined
1567
1568 if (options.toolChoice === "auto") {
1569 toolChoice = { type: "auto" }
1570 } else if (options.toolChoice === "required") {
1571 toolChoice = { type: "any" }
1572 } else if ("tool" in options.toolChoice) {
1573 toolChoice = { type: "tool", name: options.toolChoice.tool }
1574 } else {
1575 const allowedTools = new Set(options.toolChoice.oneOf)
1576 tools = tools.filter((tool) => allowedTools.has(tool.name))
1577 toolChoice = { type: options.toolChoice.mode === "required" ? "any" : "auto" }
1578 }
1579
1580 if (
1581 Predicate.isNotUndefined(config.disableParallelToolCalls) &&
1582 Predicate.isNotUndefined(toolChoice) &&
1583 toolChoice.type !== "none"
1584 ) {
1585 toolChoice.disable_parallel_tool_use = config.disableParallelToolCalls
1586 }
1587
1588 return {
1589 tools,
1590 toolChoice
1591 }
1592 }
1593)
1594
1595// =============================================================================
1596// HTTP Details
1597// =============================================================================
1598
1599const buildHttpRequestDetails = (
1600 request: HttpClientRequest.HttpClientRequest
1601): typeof Response.HttpRequestDetails.Type => ({
1602 method: request.method,
1603 url: request.url,
1604 urlParams: Array.from(request.urlParams),
1605 hash: Option.getOrUndefined(request.hash),
1606 headers: Redactable.redact(request.headers) as Record<string, string>
1607})
1608
1609const buildHttpResponseDetails = (
1610 response: HttpClientResponse.HttpClientResponse
1611): typeof Response.HttpResponseDetails.Type => ({
1612 status: response.status,
1613 headers: Redactable.redact(response.headers) as Record<string, string>
1614})
1615
1616// =============================================================================
1617// Response Conversion
1618// =============================================================================
1619
1620const makeResponse = Effect.fnUntraced(
1621 function*<Tools extends ReadonlyArray<Tool.Any>>({
1622 options,
1623 rawResponse,
1624 response,
1625 toolNameMapper
1626 }: {
1627 readonly options: LanguageModel.ProviderOptions
1628 readonly rawResponse: Generated.BetaMessage
1629 readonly response: HttpClientResponse.HttpClientResponse
1630 readonly toolNameMapper: Tool.NameMapper<Tools>
1631 }): Effect.fn.Return<
1632 Array<Response.PartEncoded>,
1633 AiError.AiError,
1634 IdGenerator.IdGenerator
1635 > {
1636 const parts: Array<Response.PartEncoded> = []
1637 const mcpToolCalls: Map<string, Response.ToolCallPartEncoded> = new Map()
1638 const serverToolCalls: Map<string, string> = new Map()
1639 const citableDocuments = extractCitableDocuments(options.prompt)
1640 const responseFormat = options.responseFormat
1641 const hasStructuredOutputTool = responseFormat.type === "json" &&
1642 rawResponse.content.some((part) => part.type === "tool_use" && part.name === responseFormat.objectName)
1643
1644 parts.push({
1645 type: "response-metadata",
1646 id: rawResponse.id,
1647 modelId: rawResponse.model,
1648 timestamp: DateTime.formatIso(yield* DateTime.now),
1649 request: buildHttpRequestDetails(response.request)
1650 })
1651
1652 for (const part of rawResponse.content) {
1653 switch (part.type) {
1654 case "text": {
1655 // The response tool supplies the JSON payload. Accompanying prose
1656 // must not be concatenated with it during structured output decoding.
1657 if (hasStructuredOutputTool) {
1658 break
1659 }
1660
1661 parts.push({
1662 type: "text",
1663 text: part.text
1664 })
1665
1666 if (Predicate.isNotNullish(part.citations)) {
1667 for (const citation of part.citations) {
1668 const source = yield* processCitation(citation, citableDocuments)
1669 if (Predicate.isNotUndefined(source)) {
1670 parts.push(source)
1671 }
1672 }
1673 }
1674
1675 break
1676 }
1677
1678 case "thinking": {
1679 const metadata = {
1680 info: { type: "thinking", signature: part.signature }
1681 } as const
1682
1683 parts.push({
1684 type: "reasoning",
1685 text: part.thinking,
1686 metadata: { anthropic: metadata }
1687 })
1688 break
1689 }
1690
1691 case "redacted_thinking": {
1692 const metadata = {
1693 info: { type: "redacted_thinking", redactedData: part.data }
1694 } as const
1695
1696 parts.push({
1697 type: "reasoning",
1698 text: "",
1699 metadata: { anthropic: metadata }
1700 })
1701 break
1702 }
1703
1704 case "tool_use": {
1705 // When the `"json"` response format is requested, the JSON we need
1706 // is returned by a tool call injected into the request
1707 if (responseFormat.type === "json" && part.name === responseFormat.objectName) {
1708 parts.push({
1709 type: "text",
1710 text: JSON.stringify(part.input)
1711 })
1712 } else {
1713 // Extract caller info if present
1714 const caller = (part as any).caller
1715 const callerInfo = Predicate.isNotNullish(caller)
1716 ? {
1717 type: caller.type,
1718 toolId: "tool_id" in caller ? caller.tool_id : null
1719 }
1720 : undefined
1721
1722 // Map the provider wire name (e.g. "memory") back to the tool's
1723 // custom name (e.g. "AnthropicMemory") that the toolkit is keyed by
1724 const toolName = toolNameMapper.getCustomName(part.name)
1725 const params = yield* transformToolCallParams(options.tools, toolName, part.input)
1726
1727 parts.push({
1728 type: "tool-call",
1729 id: part.id,
1730 name: toolName,
1731 params,
1732 ...(Predicate.isNotUndefined(callerInfo)
1733 ? { metadata: { anthropic: { caller: callerInfo } } }
1734 : undefined)
1735 })
1736 }
1737
1738 break
1739 }
1740
1741 case "server_tool_use": {
1742 const toolName = toolNameMapper.getCustomName(part.name)
1743
1744 if (
1745 part.name === "bash_code_execution" ||
1746 part.name === "text_editor_code_execution"
1747 ) {
1748 parts.push({
1749 type: "tool-call",
1750 id: part.id,
1751 name: toolName,
1752 params: { type: part.name, ...part.input },
1753 providerExecuted: true
1754 })
1755 } else if (
1756 part.name === "code_execution" ||
1757 part.name === "web_fetch" ||
1758 part.name === "web_search"
1759 ) {
1760 const toolParams: Record<string, unknown> = { ...part.input }
1761
1762 // Inject `type: "programmatic-tool-call"` when the input parameters
1763 // has the format `{ code: ... }`
1764 if (
1765 part.name === "code_execution" &&
1766 Predicate.hasProperty(part.input, "code") &&
1767 !Predicate.hasProperty(part.input, "type")
1768 ) {
1769 toolParams.type = "programmatic-tool-call"
1770 }
1771
1772 parts.push({
1773 type: "tool-call",
1774 id: part.id,
1775 name: toolName,
1776 params: toolParams,
1777 providerExecuted: true
1778 })
1779 } else if (
1780 part.name === "tool_search_tool_bm25" ||
1781 part.name === "tool_search_tool_regex"
1782 ) {
1783 serverToolCalls.set(part.id, part.name)
1784 parts.push({
1785 type: "tool-call",
1786 id: part.id,
1787 name: toolName,
1788 params: part.input,
1789 providerExecuted: true
1790 })
1791 }
1792
1793 break
1794 }
1795
1796 case "mcp_tool_use": {
1797 const toolCall: Response.ToolCallPartEncoded = {
1798 type: "tool-call",
1799 id: part.id,
1800 name: part.name,
1801 params: part.input,
1802 providerExecuted: true,
1803 metadata: { anthropic: { mcp_tool: { server: part.server_name } } }
1804 }
1805
1806 mcpToolCalls.set(part.id, toolCall)
1807
1808 parts.push(toolCall)
1809
1810 break
1811 }
1812
1813 case "mcp_tool_result": {
1814 const toolCall = mcpToolCalls.get(part.tool_use_id)
1815 const mcpMetadata = toolCall?.metadata?.anthropic?.mcp_tool
1816
1817 if (Predicate.isNotUndefined(toolCall)) {
1818 parts.push({
1819 type: "tool-result",
1820 id: part.tool_use_id,
1821 name: toolCall.name,
1822 isFailure: part.is_error,
1823 result: part.content,
1824 metadata: {
1825 anthropic: {
1826 ...(Predicate.isNotNullish(mcpMetadata)
1827 ? { mcp_tool: mcpMetadata } :
1828 undefined)
1829 }
1830 }
1831 })
1832 }
1833
1834 break
1835 }
1836
1837 // Code Execution 20250522
1838 case "code_execution_tool_result": {
1839 const toolName = toolNameMapper.getCustomName("code_execution")
1840
1841 if (part.content.type === "code_execution_result") {
1842 parts.push({
1843 type: "tool-result",
1844 id: part.tool_use_id,
1845 name: toolName,
1846 isFailure: false,
1847 result: part.content
1848 })
1849 } else {
1850 parts.push({
1851 type: "tool-result",
1852 id: part.tool_use_id,
1853 name: toolName,
1854 isFailure: true,
1855 result: part.content
1856 })
1857 }
1858
1859 break
1860 }
1861
1862 // Code Execution 20250825
1863 case "bash_code_execution_tool_result":
1864 case "text_editor_code_execution_tool_result": {
1865 const toolName = toolNameMapper.getCustomName("code_execution")
1866
1867 if (
1868 part.content.type === "bash_code_execution_tool_result_error" ||
1869 part.content.type === "text_editor_code_execution_tool_result_error"
1870 ) {
1871 parts.push({
1872 type: "tool-result",
1873 id: part.tool_use_id,
1874 name: toolName,
1875 isFailure: true,
1876 result: part.content
1877 })
1878 } else {
1879 parts.push({
1880 type: "tool-result",
1881 id: part.tool_use_id,
1882 name: toolName,
1883 isFailure: false,
1884 result: part.content
1885 })
1886 }
1887
1888 break
1889 }
1890
1891 case "tool_search_tool_result": {
1892 let providerName = serverToolCalls.get(part.tool_use_id)
1893
1894 if (Predicate.isUndefined(providerName)) {
1895 const bm25Name = toolNameMapper.getCustomName("tool_search_tool_bm25")
1896 const regexName = toolNameMapper.getCustomName("tool_search_tool_regex")
1897
1898 if (bm25Name !== "tool_search_tool_bm25") {
1899 providerName = "tool_search_tool_bm25"
1900 } else if (regexName !== "tool_search_tool_regex") {
1901 providerName = "tool_search_tool_regex"
1902 }
1903 }
1904
1905 const toolName = toolNameMapper.getCustomName(providerName!)
1906
1907 if (part.content.type === "tool_search_tool_search_result") {
1908 parts.push({
1909 type: "tool-result",
1910 id: part.tool_use_id,
1911 name: toolName,
1912 isFailure: false,
1913 result: part.content.tool_references
1914 })
1915 } else {
1916 parts.push({
1917 type: "tool-result",
1918 id: part.tool_use_id,
1919 name: toolName,
1920 isFailure: true,
1921 result: part.content
1922 })
1923 }
1924
1925 break
1926 }
1927
1928 case "web_fetch_tool_result": {
1929 const toolName = toolNameMapper.getCustomName("web_fetch")
1930
1931 if (part.content.type === "web_fetch_result") {
1932 citableDocuments.push({
1933 title: part.content.content.title ?? part.content.url,
1934 mediaType: part.content.content.source.media_type
1935 })
1936
1937 parts.push({
1938 type: "tool-result",
1939 id: part.tool_use_id,
1940 name: toolName,
1941 isFailure: false,
1942 result: part.content
1943 })
1944 } else {
1945 parts.push({
1946 type: "tool-result",
1947 id: part.tool_use_id,
1948 name: toolName,
1949 isFailure: true,
1950 result: part.content
1951 })
1952 }
1953
1954 break
1955 }
1956
1957 case "web_search_tool_result": {
1958 const toolName = toolNameMapper.getCustomName("web_search")
1959
1960 if (Predicate.hasProperty(part.content, "type")) {
1961 parts.push({
1962 type: "tool-result",
1963 id: part.tool_use_id,
1964 name: toolName,
1965 isFailure: true,
1966 result: part.content
1967 })
1968 } else {
1969 const idGenerator = yield* IdGenerator.IdGenerator
1970
1971 parts.push({
1972 type: "tool-result",
1973 id: part.tool_use_id,
1974 name: toolName,
1975 isFailure: false,
1976 result: part.content
1977 })
1978
1979 const content = part.content as ReadonlyArray<Generated.BetaResponseWebSearchResultBlock>
1980
1981 for (const result of content) {
1982 const id = yield* idGenerator.generateId()
1983
1984 parts.push({
1985 type: "source",
1986 sourceType: "url",
1987 id,
1988 url: result.url,
1989 title: result.title,
1990 metadata: { anthropic: { source: "web", pageAge: result.page_age } }
1991 })
1992 }
1993 }
1994
1995 break
1996 }
1997 }
1998 }
1999
2000 // Anthropic always returns a non-null `stop_reason` for non-streaming responses
2001 const finishReason = InternalUtilities.resolveFinishReason(
2002 rawResponse.stop_reason!,
2003 options.responseFormat.type === "json"
2004 )
2005
2006 const inputTokens = rawResponse.usage.input_tokens
2007 const outputTokens = rawResponse.usage.output_tokens
2008 const cacheWriteTokens = rawResponse.usage.cache_creation_input_tokens ?? 0
2009 const cacheReadTokens = rawResponse.usage.cache_read_input_tokens ?? 0
2010
2011 parts.push({
2012 type: "finish",
2013 reason: finishReason,
2014 usage: {
2015 inputTokens: {
2016 uncached: inputTokens,
2017 total: inputTokens + cacheWriteTokens + cacheReadTokens,
2018 cacheRead: cacheReadTokens,
2019 cacheWrite: cacheWriteTokens
2020 },
2021 outputTokens: {
2022 total: outputTokens,
2023 text: undefined,
2024 reasoning: undefined
2025 }
2026 },
2027 response: buildHttpResponseDetails(response),
2028 metadata: {
2029 anthropic: {
2030 container: rawResponse.container ?? null,
2031 contextManagement: rawResponse.context_management ?? null,
2032 usage: rawResponse.usage,
2033 stopSequence: rawResponse.stop_sequence
2034 }
2035 }
2036 })
2037
2038 return parts
2039 }
2040)
2041
2042const makeStreamResponse = Effect.fnUntraced(
2043 function*<Tools extends ReadonlyArray<Tool.Any>>({
2044 stream,
2045 response,
2046 options,
2047 toolNameMapper
2048 }: {
2049 readonly stream: Stream.Stream<MessageStreamEvent, AiError.AiError>
2050 readonly response: HttpClientResponse.HttpClientResponse
2051 readonly options: LanguageModel.ProviderOptions
2052 readonly toolNameMapper: Tool.NameMapper<Tools>
2053 }): Effect.fn.Return<
2054 Stream.Stream<Response.StreamPartEncoded, AiError.AiError, IdGenerator.IdGenerator>,
2055 AiError.AiError
2056 > {
2057 const citableDocuments = extractCitableDocuments(options.prompt)
2058
2059 let container: typeof Generated.BetaContainer.Encoded | null = null
2060 let contextManagement: typeof Generated.BetaResponseContextManagement.Encoded | null = null
2061 let finishReason: Response.FinishReason = "unknown"
2062 let stopSequence: string | null = null
2063 let rawUsage: typeof Generated.BetaMessage.Encoded["usage"] | null = null
2064 const mcpToolCalls: Map<string, Response.ToolCallPartEncoded> = new Map()
2065 const serverToolCalls: Map<string, string> = new Map()
2066 const contentBlocks: Map<
2067 number,
2068 | {
2069 readonly type: "text"
2070 }
2071 | {
2072 readonly type: "reasoning"
2073 }
2074 | {
2075 readonly type: "tool-call"
2076 readonly id: string
2077 readonly name: string
2078 params: string
2079 firstDelta: boolean
2080 readonly providerName?: string | undefined
2081 readonly providerExecuted?: boolean | undefined
2082 readonly caller?: { type: string; toolId: string | null } | undefined
2083 }
2084 > = new Map()
2085 const usage: Mutable<{
2086 inputTokens: number
2087 outputTokens: number
2088 cacheReadInputTokens: number
2089 cacheWriteInputTokens: number
2090 }> = {
2091 inputTokens: 0,
2092 outputTokens: 0,
2093 cacheReadInputTokens: 0,
2094 cacheWriteInputTokens: 0
2095 }
2096
2097 let blockType: typeof Generated.BetaContentBlockStartEvent.Encoded["content_block"]["type"] | undefined = undefined
2098
2099 return stream.pipe(
2100 Stream.mapEffect(Effect.fnUntraced(function*(event) {
2101 const parts: Array<Response.StreamPartEncoded> = []
2102
2103 switch (event.type) {
2104 case "message_start": {
2105 rawUsage = { ...event.message.usage }
2106 usage.inputTokens = event.message.usage.input_tokens
2107 usage.cacheReadInputTokens = event.message.usage.cache_read_input_tokens ?? 0
2108 usage.cacheWriteInputTokens = event.message.usage.cache_creation_input_tokens ?? 0
2109
2110 if (Predicate.isNotNullish(event.message.container)) {
2111 container = event.message.container
2112 }
2113
2114 if (Predicate.isNotNull(event.message.stop_sequence)) {
2115 stopSequence = event.message.stop_sequence
2116 }
2117
2118 if (Predicate.isNotNull(event.message.stop_reason)) {
2119 finishReason = InternalUtilities.resolveFinishReason(event.message.stop_reason)
2120 }
2121
2122 parts.push({
2123 type: "response-metadata",
2124 id: event.message.id,
2125 modelId: event.message.model,
2126 timestamp: DateTime.formatIso(yield* DateTime.now),
2127 request: buildHttpRequestDetails(response.request)
2128 })
2129
2130 // Process pre-populated content blocks
2131 if (Predicate.isNotNullish(event.message.content) && event.message.content.length > 0) {
2132 for (let i = 0; i < event.message.content.length; i++) {
2133 const part = event.message.content[i]
2134
2135 if (part.type === "tool_use") {
2136 const callerInfo = Predicate.isNotUndefined(part.caller)
2137 ? {
2138 type: part.caller.type,
2139 toolId: "tool_id" in part.caller ? part.caller.tool_id : null
2140 }
2141 : undefined
2142
2143 // Map the provider wire name (e.g. "memory") back to the
2144 // tool's custom name (e.g. "AnthropicMemory") that the toolkit
2145 // is keyed by
2146 const toolName = toolNameMapper.getCustomName(part.name)
2147
2148 parts.push({
2149 type: "tool-params-start",
2150 id: part.id,
2151 name: toolName
2152 })
2153
2154 parts.push({
2155 type: "tool-params-delta",
2156 id: part.id,
2157 delta: JSON.stringify(part.input ?? {})
2158 })
2159
2160 parts.push({
2161 type: "tool-params-end",
2162 id: part.id
2163 })
2164
2165 const params = yield* transformToolCallParams(options.tools, toolName, part.input)
2166
2167 parts.push({
2168 type: "tool-call",
2169 id: part.id,
2170 name: toolName,
2171 params,
2172 ...(Predicate.isNotUndefined(callerInfo)
2173 ? { metadata: { anthropic: { caller: callerInfo } } }
2174 : undefined)
2175 })
2176 }
2177 }
2178 }
2179
2180 break
2181 }
2182
2183 case "message_delta": {
2184 rawUsage = { ...rawUsage, ...event.usage } as any
2185
2186 if (
2187 Predicate.isNotNull(event.usage.input_tokens) &&
2188 usage.inputTokens !== event.usage.input_tokens
2189 ) {
2190 usage.inputTokens = event.usage.input_tokens
2191 }
2192 usage.outputTokens = event.usage.output_tokens
2193
2194 if (
2195 Predicate.isNotNull(event.usage.cache_read_input_tokens) &&
2196 usage.cacheReadInputTokens !== event.usage.cache_read_input_tokens
2197 ) {
2198 usage.cacheReadInputTokens = event.usage.cache_read_input_tokens
2199 }
2200 if (
2201 Predicate.isNotNull(event.usage.cache_creation_input_tokens) &&
2202 usage.cacheWriteInputTokens !== event.usage.cache_creation_input_tokens
2203 ) {
2204 usage.cacheWriteInputTokens = event.usage.cache_creation_input_tokens
2205 }
2206
2207 if (Predicate.isNotNullish(event.delta.container)) {
2208 container = event.delta.container
2209 }
2210
2211 if (Predicate.isNotNullish(event.context_management)) {
2212 contextManagement = event.context_management
2213 }
2214
2215 if (Predicate.isNotNull(event.delta.stop_reason)) {
2216 finishReason = InternalUtilities.resolveFinishReason(event.delta.stop_reason)
2217 }
2218
2219 if (Predicate.isNotNull(event.delta.stop_sequence)) {
2220 stopSequence = event.delta.stop_sequence
2221 }
2222
2223 break
2224 }
2225
2226 case "message_stop": {
2227 const metadata: Response.FinishPartMetadata = {
2228 anthropic: {
2229 container,
2230 contextManagement,
2231 stopSequence,
2232 usage: rawUsage
2233 }
2234 }
2235
2236 parts.push({
2237 type: "finish",
2238 reason: finishReason,
2239 usage: {
2240 inputTokens: {
2241 uncached: usage.inputTokens,
2242 total: usage.inputTokens + usage.cacheWriteInputTokens + usage.cacheReadInputTokens,
2243 cacheRead: usage.cacheReadInputTokens,
2244 cacheWrite: usage.cacheWriteInputTokens
2245 },
2246 outputTokens: {
2247 total: usage.outputTokens,
2248 text: undefined,
2249 reasoning: undefined
2250 }
2251 },
2252 response: buildHttpResponseDetails(response),
2253 metadata
2254 })
2255
2256 break
2257 }
2258
2259 case "content_block_start": {
2260 blockType = event.content_block.type
2261
2262 switch (event.content_block.type) {
2263 case "text": {
2264 contentBlocks.set(event.index, { type: "text" })
2265
2266 parts.push({
2267 type: "text-start",
2268 id: event.index.toString()
2269 })
2270
2271 break
2272 }
2273
2274 case "thinking": {
2275 contentBlocks.set(event.index, { type: "reasoning" })
2276
2277 parts.push({
2278 type: "reasoning-start",
2279 id: event.index.toString()
2280 })
2281
2282 break
2283 }
2284
2285 case "redacted_thinking": {
2286 contentBlocks.set(event.index, { type: "reasoning" })
2287
2288 const metadata: Response.ReasoningStartPartMetadata = {
2289 anthropic: {
2290 info: {
2291 type: "redacted_thinking",
2292 redactedData: event.content_block.data
2293 }
2294 }
2295 }
2296
2297 parts.push({
2298 type: "reasoning-start",
2299 id: event.index.toString(),
2300 metadata
2301 })
2302
2303 break
2304 }
2305
2306 case "tool_use": {
2307 const part = event.content_block
2308
2309 const caller = Predicate.isNotUndefined(part.caller)
2310 ? {
2311 type: part.caller.type,
2312 toolId: "tool_id" in part.caller ? part.caller.tool_id : null
2313 }
2314 : undefined
2315
2316 const hasParams = Object.keys(part.input).length > 0
2317 const initialParams = hasParams ? JSON.stringify(part.input) : ""
2318 // Map the provider wire name (e.g. "memory") back to the tool's
2319 // custom name (e.g. "AnthropicMemory") that the toolkit is keyed
2320 // by. The mapped name flows to the finalized tool-call via
2321 // `contentBlock.name` on `content_block_stop`.
2322 const toolName = toolNameMapper.getCustomName(part.name)
2323 contentBlocks.set(event.index, {
2324 type: "tool-call",
2325 id: part.id,
2326 name: toolName,
2327 params: initialParams,
2328 firstDelta: initialParams.length > 0,
2329 ...(Predicate.isNotUndefined(caller) ? { caller } : undefined)
2330 })
2331
2332 parts.push({
2333 type: "tool-params-start",
2334 id: part.id,
2335 name: toolName
2336 })
2337
2338 break
2339 }
2340
2341 case "server_tool_use": {
2342 const part = event.content_block
2343
2344 if (
2345 part.name === "code_execution" ||
2346 part.name === "bash_code_execution" ||
2347 part.name === "text_editor_code_execution" ||
2348 part.name === "web_fetch" ||
2349 part.name === "web_search"
2350 ) {
2351 const toolName = toolNameMapper.getCustomName(
2352 part.name === "bash_code_execution" || part.name === "text_editor_code_execution"
2353 ? "code_execution"
2354 : part.name
2355 )
2356
2357 contentBlocks.set(event.index, {
2358 type: "tool-call",
2359 id: part.id,
2360 name: toolName,
2361 params: "",
2362 firstDelta: true,
2363 providerName: part.name,
2364 providerExecuted: true
2365 })
2366
2367 parts.push({
2368 type: "tool-params-start",
2369 id: part.id,
2370 name: toolName,
2371 providerExecuted: true
2372 })
2373 } else if (
2374 part.name === "tool_search_tool_bm25" ||
2375 part.name === "tool_search_tool_regex"
2376 ) {
2377 serverToolCalls.set(part.id, part.name)
2378
2379 const toolName = toolNameMapper.getCustomName(part.name)
2380
2381 contentBlocks.set(event.index, {
2382 type: "tool-call",
2383 id: part.id,
2384 name: toolName,
2385 params: "",
2386 firstDelta: true,
2387 providerName: part.name,
2388 providerExecuted: true
2389 })
2390
2391 parts.push({
2392 type: "tool-params-start",
2393 id: part.id,
2394 name: toolName,
2395 providerExecuted: true
2396 })
2397 }
2398
2399 break
2400 }
2401
2402 case "web_fetch_tool_result": {
2403 const part = event.content_block
2404 const toolName = toolNameMapper.getCustomName("web_fetch")
2405
2406 if (part.content.type === "web_fetch_result") {
2407 citableDocuments.push({
2408 title: part.content.content.title ?? part.content.url,
2409 mediaType: part.content.content.source.media_type
2410 })
2411
2412 parts.push({
2413 type: "tool-result",
2414 id: part.tool_use_id,
2415 name: toolName,
2416 isFailure: false,
2417 result: part.content
2418 })
2419 } else {
2420 parts.push({
2421 type: "tool-result",
2422 id: part.tool_use_id,
2423 name: toolName,
2424 isFailure: true,
2425 result: part.content
2426 })
2427 }
2428
2429 break
2430 }
2431
2432 case "web_search_tool_result": {
2433 const part = event.content_block
2434 const toolName = toolNameMapper.getCustomName("web_search")
2435
2436 if (Predicate.hasProperty(part.content, "type")) {
2437 parts.push({
2438 type: "tool-result",
2439 id: part.tool_use_id,
2440 name: toolName,
2441 isFailure: true,
2442 result: part.content
2443 })
2444 } else {
2445 const idGenerator = yield* IdGenerator.IdGenerator
2446
2447 parts.push({
2448 type: "tool-result",
2449 id: part.tool_use_id,
2450 name: toolName,
2451 isFailure: false,
2452 result: part.content
2453 })
2454
2455 const content = part.content as ReadonlyArray<Generated.BetaResponseWebSearchResultBlock>
2456
2457 for (const result of content) {
2458 const id = yield* idGenerator.generateId()
2459
2460 parts.push({
2461 type: "source",
2462 sourceType: "url",
2463 id,
2464 url: result.url,
2465 title: result.title,
2466 metadata: { anthropic: { source: "web", pageAge: result.page_age } }
2467 })
2468 }
2469 }
2470 break
2471 }
2472
2473 case "code_execution_tool_result": {
2474 const part = event.content_block
2475 const toolName = toolNameMapper.getCustomName("code_execution")
2476
2477 if (part.content.type === "code_execution_result") {
2478 parts.push({
2479 type: "tool-result",
2480 id: part.tool_use_id,
2481 name: toolName,
2482 isFailure: false,
2483 result: part.content
2484 })
2485 } else {
2486 parts.push({
2487 type: "tool-result",
2488 id: part.tool_use_id,
2489 name: toolName,
2490 isFailure: true,
2491 result: part.content
2492 })
2493 }
2494 break
2495 }
2496
2497 case "bash_code_execution_tool_result":
2498 case "text_editor_code_execution_tool_result": {
2499 const part = event.content_block
2500 const toolName = toolNameMapper.getCustomName("code_execution")
2501
2502 if (
2503 part.content.type === "bash_code_execution_tool_result_error" ||
2504 part.content.type === "text_editor_code_execution_tool_result_error"
2505 ) {
2506 parts.push({
2507 type: "tool-result",
2508 id: part.tool_use_id,
2509 name: toolName,
2510 isFailure: true,
2511 result: part.content
2512 })
2513 } else {
2514 parts.push({
2515 type: "tool-result",
2516 id: part.tool_use_id,
2517 name: toolName,
2518 isFailure: false,
2519 result: part.content
2520 })
2521 }
2522 break
2523 }
2524
2525 case "tool_search_tool_result": {
2526 const part = event.content_block
2527 let providerName = serverToolCalls.get(part.tool_use_id)
2528
2529 if (Predicate.isUndefined(providerName)) {
2530 const bm25Name = toolNameMapper.getCustomName("tool_search_tool_bm25")
2531 const regexName = toolNameMapper.getCustomName("tool_search_tool_regex")
2532
2533 if (bm25Name !== "tool_search_tool_bm25") {
2534 providerName = "tool_search_tool_bm25"
2535 } else if (regexName !== "tool_search_tool_regex") {
2536 providerName = "tool_search_tool_regex"
2537 }
2538 }
2539
2540 const toolName = toolNameMapper.getCustomName(providerName!)
2541
2542 if (part.content.type === "tool_search_tool_search_result") {
2543 parts.push({
2544 type: "tool-result",
2545 id: part.tool_use_id,
2546 name: toolName,
2547 isFailure: false,
2548 result: part.content.tool_references
2549 })
2550 } else {
2551 parts.push({
2552 type: "tool-result",
2553 id: part.tool_use_id,
2554 name: toolName,
2555 isFailure: true,
2556 result: part.content
2557 })
2558 }
2559 break
2560 }
2561
2562 case "mcp_tool_use": {
2563 const part = event.content_block
2564
2565 const toolCall: Response.ToolCallPartEncoded = {
2566 type: "tool-call",
2567 id: part.id,
2568 name: part.name,
2569 params: part.input,
2570 providerExecuted: true,
2571 metadata: { anthropic: { mcp_tool: { server: part.server_name } } }
2572 }
2573
2574 mcpToolCalls.set(part.id, toolCall)
2575
2576 parts.push(toolCall)
2577
2578 break
2579 }
2580
2581 case "mcp_tool_result": {
2582 const part = event.content_block
2583 const toolCall = mcpToolCalls.get(part.tool_use_id)
2584 const mcpMetadata = toolCall?.metadata?.anthropic?.mcp_tool
2585
2586 if (Predicate.isNotUndefined(toolCall)) {
2587 parts.push({
2588 type: "tool-result",
2589 id: part.tool_use_id,
2590 name: toolCall.name,
2591 isFailure: part.is_error,
2592 result: part.content,
2593 metadata: {
2594 anthropic: {
2595 ...(Predicate.isNotNullish(mcpMetadata)
2596 ? { mcp_tool: mcpMetadata } :
2597 undefined)
2598 }
2599 }
2600 })
2601 }
2602
2603 break
2604 }
2605 }
2606
2607 break
2608 }
2609
2610 case "content_block_delta": {
2611 const part = event.delta
2612
2613 switch (part.type) {
2614 case "text_delta": {
2615 parts.push({
2616 type: "text-delta",
2617 id: event.index.toString(),
2618 delta: part.text
2619 })
2620
2621 break
2622 }
2623
2624 case "thinking_delta": {
2625 parts.push({
2626 type: "reasoning-delta",
2627 id: event.index.toString(),
2628 delta: part.thinking
2629 })
2630
2631 break
2632 }
2633
2634 case "signature_delta": {
2635 if (blockType === "thinking") {
2636 parts.push({
2637 type: "reasoning-delta",
2638 id: event.index.toString(),
2639 delta: "",
2640 metadata: {
2641 anthropic: {
2642 info: {
2643 type: "thinking",
2644 signature: part.signature
2645 }
2646 }
2647 }
2648 })
2649 }
2650
2651 break
2652 }
2653
2654 case "input_json_delta": {
2655 let delta = part.partial_json
2656
2657 // Skip empty deltas
2658 if (delta.length === 0) {
2659 break
2660 }
2661
2662 const contentBlock = contentBlocks.get(event.index)
2663
2664 // Skip invalid deltas
2665 if (Predicate.isUndefined(contentBlock)) {
2666 break
2667 }
2668
2669 // Skip non-tool-call deltas
2670 if (contentBlock.type !== "tool-call") {
2671 break
2672 }
2673
2674 if (
2675 contentBlock.firstDelta &&
2676 (contentBlock.providerName === "bash_code_execution" ||
2677 contentBlock.providerName === "text_editor_code_execution")
2678 ) {
2679 delta = `{"type":${JSON.stringify(contentBlock.providerName)},${delta.substring(1)}`
2680 }
2681
2682 parts.push({
2683 type: "tool-params-delta",
2684 id: contentBlock.id,
2685 delta
2686 })
2687
2688 contentBlock.params += delta
2689 contentBlock.firstDelta = false
2690
2691 break
2692 }
2693
2694 case "citations_delta": {
2695 const source = yield* processCitation(part.citation, citableDocuments)
2696
2697 if (Predicate.isNotUndefined(source)) {
2698 parts.push(source)
2699 }
2700
2701 break
2702 }
2703 }
2704
2705 break
2706 }
2707
2708 case "content_block_stop": {
2709 const contentBlock = contentBlocks.get(event.index)
2710
2711 if (Predicate.isNotUndefined(contentBlock)) {
2712 switch (contentBlock.type) {
2713 case "text": {
2714 parts.push({
2715 type: "text-end",
2716 id: event.index.toString()
2717 })
2718
2719 break
2720 }
2721
2722 case "reasoning": {
2723 parts.push({
2724 type: "reasoning-end",
2725 id: event.index.toString()
2726 })
2727
2728 break
2729 }
2730
2731 case "tool-call": {
2732 parts.push({
2733 type: "tool-params-end",
2734 id: contentBlock.id
2735 })
2736
2737 // For code execution, inject the `programmatic-tool-call` type
2738 // when the input format is `{ code }`
2739 let finalParams = contentBlock.params.length === 0 ? "{}" : contentBlock.params
2740
2741 if (contentBlock.providerName === "code_execution") {
2742 // @effect-diagnostics-next-line tryCatchInEffectGen:off
2743 try {
2744 const params = Tool.unsafeSecureJsonParse(finalParams)
2745 if (Predicate.hasProperty(params, "code") && !Predicate.hasProperty(params, "type")) {
2746 finalParams = JSON.stringify({ type: "programmatic-tool-call", ...params })
2747 }
2748 } catch {
2749 // Ignore errors and use original tool call parameters
2750 }
2751 }
2752
2753 const toolParams = yield* Effect.try({
2754 try: () => Tool.unsafeSecureJsonParse(finalParams),
2755 catch: (cause) =>
2756 AiError.make({
2757 module: "AnthropicLanguageModel",
2758 method: "makeStreamResponse",
2759 reason: new AiError.ToolParameterValidationError({
2760 toolName: contentBlock.name,
2761 description: `Failed to securely JSON parse tool parameters: ${cause}`
2762 })
2763 })
2764 })
2765
2766 const params = contentBlock.providerExecuted === true
2767 ? toolParams
2768 : yield* transformToolCallParams(options.tools, contentBlock.name, toolParams)
2769
2770 parts.push({
2771 type: "tool-call",
2772 id: contentBlock.id,
2773 name: contentBlock.name,
2774 params,
2775 ...(Predicate.isNotUndefined(contentBlock.providerExecuted)
2776 ? { providerExecuted: contentBlock.providerExecuted }
2777 : undefined),
2778 ...(Predicate.isNotUndefined(contentBlock.caller)
2779 ? { metadata: { anthropic: { caller: contentBlock.caller } } }
2780 : undefined)
2781 })
2782 }
2783 }
2784
2785 contentBlocks.delete(event.index)
2786 }
2787
2788 blockType = undefined
2789
2790 break
2791 }
2792
2793 case "error": {
2794 parts.push({
2795 type: "error",
2796 error: event.error,
2797 metadata: { anthropic: { requestId: event.request_id } }
2798 })
2799
2800 break
2801 }
2802 }
2803
2804 return parts
2805 })),
2806 Stream.flattenIterable
2807 )
2808 }
2809)
2810
2811// =============================================================================
2812// Telemetry
2813// =============================================================================
2814
2815const annotateRequest = (
2816 span: Span,
2817 request: typeof Generated.BetaCreateMessageParams.Encoded
2818): void => {
2819 addGenAIAnnotations(span, {
2820 system: "anthropic",
2821 operation: { name: "chat" },
2822 request: {
2823 model: request.model,
2824 temperature: request.temperature,
2825 topK: request.top_k,
2826 topP: request.top_p,
2827 maxTokens: request.max_tokens,
2828 stopSequences: Arr.ensure(request.stop_sequences).filter(
2829 Predicate.isNotNullish
2830 )
2831 }
2832 })
2833}
2834
2835const annotateResponse = (span: Span, response: Generated.BetaMessage): void => {
2836 addGenAIAnnotations(span, {
2837 response: {
2838 id: response.id,
2839 model: response.model,
2840 finishReasons: response.stop_reason ? [response.stop_reason] : undefined
2841 },
2842 usage: {
2843 inputTokens: response.usage.input_tokens,
2844 outputTokens: response.usage.output_tokens
2845 }
2846 })
2847}
2848
2849const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => {
2850 if (part.type === "response-metadata") {
2851 addGenAIAnnotations(span, {
2852 response: {
2853 id: part.id,
2854 model: part.modelId
2855 }
2856 })
2857 }
2858 if (part.type === "finish") {
2859 addGenAIAnnotations(span, {
2860 response: {
2861 finishReasons: [part.reason]
2862 },
2863 usage: {
2864 inputTokens: part.usage.inputTokens.uncached,
2865 outputTokens: part.usage.outputTokens.total
2866 }
2867 })
2868 }
2869}
2870
2871// =============================================================================
2872// Internal Utilities
2873// =============================================================================
2874
2875type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup
2876
2877interface SystemMessageGroup {
2878 readonly type: "system"
2879 readonly messages: Array<Prompt.SystemMessage>
2880}
2881
2882interface AssistantMessageGroup {
2883 readonly type: "assistant"
2884 readonly messages: Array<Prompt.AssistantMessage>
2885}
2886
2887interface UserMessageGroup {
2888 readonly type: "user"
2889 readonly messages: Array<Prompt.ToolMessage | Prompt.UserMessage>
2890}
2891
2892const groupMessages = (prompt: Prompt.Prompt): Array<ContentGroup> => {
2893 const messages: Array<ContentGroup> = []
2894 let current: ContentGroup | undefined = undefined
2895 for (const message of prompt.content) {
2896 switch (message.role) {
2897 case "system": {
2898 if (current?.type !== "system") {
2899 current = { type: "system", messages: [] }
2900 messages.push(current)
2901 }
2902 current.messages.push(message)
2903 break
2904 }
2905 case "assistant": {
2906 if (current?.type !== "assistant") {
2907 current = { type: "assistant", messages: [] }
2908 messages.push(current)
2909 }
2910 current.messages.push(message)
2911 break
2912 }
2913 case "tool":
2914 case "user": {
2915 if (current?.type !== "user") {
2916 current = { type: "user", messages: [] }
2917 messages.push(current)
2918 }
2919 current.messages.push(message)
2920 break
2921 }
2922 }
2923 }
2924 return messages
2925}
2926
2927/**
2928 * Checks whether data is a URL (either a URL object or a URL string).
2929 */
2930const isUrlData = (
2931 data: typeof Prompt.FilePart.Type["data"]
2932): data is URL => data instanceof URL || isUrlString(data)
2933
2934const isUrlString = (data: typeof Prompt.FilePart.Type["data"]): boolean =>
2935 typeof data === "string" && /^https?:\/\//i.test(data)
2936
2937const getUrlString = (data: string | URL): string => data instanceof URL ? data.toString() : data
2938
2939const getCacheControl = (
2940 part:
2941 | Prompt.SystemMessage
2942 | Prompt.UserMessage
2943 | Prompt.AssistantMessage
2944 | Prompt.ToolMessage
2945 | Prompt.UserMessagePart
2946 | Prompt.AssistantMessagePart
2947 | Prompt.ToolMessagePart
2948): typeof Generated.CacheControlEphemeral.Encoded | null => part.options.anthropic?.cacheControl ?? null
2949
2950const getDocumentMetadata = (part: Prompt.FilePart): {
2951 readonly title: string | null
2952 readonly context: string | null
2953} | null => {
2954 const options = part.options.anthropic
2955 if (Predicate.isNotUndefined(options)) {
2956 return {
2957 title: options?.documentTitle ?? null,
2958 context: options?.documentContext ?? null
2959 }
2960 }
2961 return null
2962}
2963
2964const areCitationsEnabled = (part: Prompt.FilePart): boolean => part.options.anthropic?.citations?.enabled ?? false
2965
2966const isCitationPart = (part: Prompt.UserMessagePart): part is Prompt.FilePart =>
2967 part.type === "file" && (part.mediaType === "application/pdf" || part.mediaType === "text/plain")
2968 ? areCitationsEnabled(part)
2969 : false
2970
2971interface CitableDocument {
2972 readonly title: string
2973 readonly fileName?: string | undefined
2974 readonly mediaType: string
2975}
2976
2977const extractCitableDocuments = (prompt: Prompt.Prompt): Array<CitableDocument> => {
2978 const citableDocuments: Array<CitableDocument> = []
2979
2980 for (const message of prompt.content) {
2981 if (message.role === "user") {
2982 for (const part of message.content) {
2983 if (isCitationPart(part)) {
2984 citableDocuments.push({
2985 title: part.fileName ?? "Untitled Document",
2986 fileName: part.fileName,
2987 mediaType: part.mediaType
2988 })
2989 }
2990 }
2991 }
2992 }
2993
2994 return citableDocuments
2995}
2996
2997const processCitation = Effect.fnUntraced(
2998 function*(
2999 citation:
3000 | Generated.ResponseCharLocationCitation
3001 | Generated.ResponsePageLocationCitation
3002 | Generated.ResponseContentBlockLocationCitation
3003 | Generated.ResponseWebSearchResultLocationCitation
3004 | Generated.ResponseSearchResultLocationCitation,
3005 citableDocuments: ReadonlyArray<CitableDocument>
3006 ): Effect.fn.Return<
3007 Response.DocumentSourcePartEncoded | Response.UrlSourcePartEncoded | undefined,
3008 never,
3009 IdGenerator.IdGenerator
3010 > {
3011 const idGenerator = yield* IdGenerator.IdGenerator
3012
3013 if (citation.type === "page_location" || citation.type === "char_location") {
3014 const citedDocument = citableDocuments[citation.document_index]
3015 if (Predicate.isNotUndefined(citedDocument)) {
3016 const id = yield* idGenerator.generateId()
3017 const metadata = citation.type === "char_location"
3018 ? {
3019 source: "document",
3020 type: citation.type,
3021 citedText: citation.cited_text,
3022 startCharIndex: citation.start_char_index,
3023 endCharIndex: citation.end_char_index
3024 } as const
3025 : {
3026 source: "document",
3027 type: citation.type,
3028 citedText: citation.cited_text,
3029 startPageNumber: citation.start_page_number,
3030 endPageNumber: citation.end_page_number
3031 } as const
3032
3033 return {
3034 type: "source",
3035 sourceType: "document",
3036 id,
3037 mediaType: citedDocument.mediaType,
3038 title: citation.document_title ?? citedDocument.title,
3039 ...(Predicate.isNotUndefined(citedDocument.fileName)
3040 ? { fileName: citedDocument.fileName }
3041 : undefined),
3042 metadata: { anthropic: metadata }
3043 }
3044 }
3045 }
3046
3047 if (citation.type === "web_search_result_location") {
3048 const id = yield* idGenerator.generateId()
3049
3050 const metadata = {
3051 source: "url",
3052 citedText: citation.cited_text,
3053 encryptedIndex: citation.encrypted_index
3054 } as const
3055
3056 return {
3057 type: "source",
3058 sourceType: "url",
3059 id,
3060 url: citation.url,
3061 title: citation.title ?? "Untitled",
3062 metadata: { anthropic: metadata }
3063 }
3064 }
3065
3066 return undefined
3067 }
3068)
3069
3070interface ModelCapabilities {
3071 readonly maxOutputTokens: number
3072 readonly supportsStructuredOutput: boolean
3073 readonly supportsMidConversationSystemMessages: boolean
3074}
3075
3076const getConfigCapabilities = (config: typeof Config.Service & { readonly model: string }): ModelCapabilities => {
3077 const capabilities = getModelCapabilities(config.model)
3078 return {
3079 ...capabilities,
3080 supportsStructuredOutput: config.structuredOutputs ?? capabilities.supportsStructuredOutput,
3081 supportsMidConversationSystemMessages: config.midConversationSystemMessages ??
3082 capabilities.supportsMidConversationSystemMessages
3083 }
3084}
3085
3086/**
3087 * Returns model defaults, optimistically assuming modern capabilities for unknown IDs.
3088 *
3089 * @see https://docs.claude.com/en/docs/about-claude/models/overview#model-comparison-table
3090 * @see https://platform.claude.com/docs/en/build-with-claude/structured-outputs
3091 * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
3092 */
3093const getModelCapabilities = (modelId: string): ModelCapabilities => {
3094 if (
3095 modelId.includes("claude-sonnet-4-5") ||
3096 modelId.includes("claude-opus-4-5") ||
3097 modelId.includes("claude-haiku-4-5")
3098 ) {
3099 return {
3100 maxOutputTokens: 64000,
3101 supportsStructuredOutput: true,
3102 supportsMidConversationSystemMessages: false
3103 }
3104 } else if (modelId.includes("claude-opus-4-1")) {
3105 return {
3106 maxOutputTokens: 32000,
3107 supportsStructuredOutput: true,
3108 supportsMidConversationSystemMessages: false
3109 }
3110 } else if (
3111 modelId.includes("claude-sonnet-4-0") ||
3112 modelId.includes("claude-sonnet-4-20250514") ||
3113 modelId.includes("claude-3-7-sonnet")
3114 ) {
3115 return {
3116 maxOutputTokens: 64000,
3117 supportsStructuredOutput: false,
3118 supportsMidConversationSystemMessages: false
3119 }
3120 } else if (
3121 modelId.includes("claude-opus-4-0") ||
3122 modelId.includes("claude-opus-4-20250514")
3123 ) {
3124 return {
3125 maxOutputTokens: 32000,
3126 supportsStructuredOutput: false,
3127 supportsMidConversationSystemMessages: false
3128 }
3129 } else if (modelId.includes("claude-3-5-haiku")) {
3130 return {
3131 maxOutputTokens: 8192,
3132 supportsStructuredOutput: false,
3133 supportsMidConversationSystemMessages: false
3134 }
3135 } else if (modelId.includes("claude-3-")) {
3136 return {
3137 maxOutputTokens: 4096,
3138 supportsStructuredOutput: false,
3139 supportsMidConversationSystemMessages: false
3140 }
3141 } else {
3142 return {
3143 maxOutputTokens: 128000,
3144 supportsStructuredOutput: true,
3145 supportsMidConversationSystemMessages: !(
3146 modelId.includes("claude-opus-4-6") ||
3147 modelId.includes("claude-opus-4-7") ||
3148 modelId.includes("claude-sonnet-4-6") ||
3149 modelId.includes("claude-mythos-preview") ||
3150 // Match Sonnet 5, excluding minor versions such as 5.5.
3151 /claude-sonnet-5(?!-\d\b)/.test(modelId)
3152 )
3153 }
3154 }
3155}
3156
3157const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError =>
3158 AiError.make({
3159 module: "AnthropicLanguageModel",
3160 method,
3161 reason: new AiError.UnsupportedSchemaError({
3162 description: error instanceof Error ? error.message : String(error)
3163 })
3164 })
3165
3166const tryCodecTransform = <S extends Schema.Constraint>(schema: S, method: string) =>
3167 Effect.try({
3168 try: () => toCodecAnthropic(schema),
3169 catch: (error) => unsupportedSchemaError(error, method)
3170 })
3171
3172const tryJsonSchema = <S extends Schema.Constraint>(schema: S, method: string) =>
3173 Effect.try({
3174 try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecAnthropic }),
3175 catch: (error) => unsupportedSchemaError(error, method)
3176 })
3177
3178const tryToolJsonSchema = <T extends Tool.Any | Tool.AnyDynamic>(tool: T, method: string) =>
3179 Effect.try({
3180 try: () => Tool.getJsonSchema(tool, { transformer: toCodecAnthropic }),
3181 catch: (error) => unsupportedSchemaError(error, method)
3182 })
3183
3184const getOutputFormat = Effect.fnUntraced(function*({ capabilities, options }: {
3185 readonly capabilities: ModelCapabilities
3186 readonly options: LanguageModel.ProviderOptions
3187}): Effect.fn.Return<typeof Generated.JsonOutputFormat.Encoded | undefined, AiError.AiError> {
3188 if (options.responseFormat.type === "json" && capabilities.supportsStructuredOutput) {
3189 const jsonSchema = yield* tryJsonSchema(options.responseFormat.schema, "getOutputFormat")
3190 return {
3191 type: "json_schema",
3192 schema: jsonSchema as any
3193 }
3194 }
3195 return undefined
3196})
3197
3198const transformToolCallParams = Effect.fnUntraced(function*<Tools extends ReadonlyArray<Tool.Any>>(
3199 tools: Tools,
3200 toolName: string,
3201 toolParams: unknown
3202): Effect.fn.Return<unknown, AiError.AiError> {
3203 const tool = tools.find((tool) => tool.name === toolName)
3204
3205 if (Predicate.isUndefined(tool)) {
3206 return yield* AiError.make({
3207 module: "AnthropicLanguageModel",
3208 method: "makeResponse",
3209 reason: new AiError.ToolNotFoundError({
3210 toolName,
3211 availableTools: tools.map((tool) => tool.name)
3212 })
3213 })
3214 }
3215
3216 const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse")
3217
3218 // Normalize valid parameters; leave invalid ones for Toolkit.
3219 return yield* (
3220 Schema.decodeEffect(codec)(toolParams) as Effect.Effect<unknown, Schema.SchemaError>
3221 ).pipe(
3222 Effect.flatMap((decoded) =>
3223 Schema.encodeUnknownEffect(tool.parametersSchema)(decoded) as Effect.Effect<unknown, Schema.SchemaError>
3224 ),
3225 Effect.orElseSucceed(() => toolParams)
3226 )
3227})