@effect/ai-anthropic 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AnthropicTool/package.json +6 -0
  2. package/dist/cjs/AnthropicClient.js +286 -190
  3. package/dist/cjs/AnthropicClient.js.map +1 -1
  4. package/dist/cjs/AnthropicLanguageModel.js +1026 -311
  5. package/dist/cjs/AnthropicLanguageModel.js.map +1 -1
  6. package/dist/cjs/AnthropicTokenizer.js +8 -6
  7. package/dist/cjs/AnthropicTokenizer.js.map +1 -1
  8. package/dist/cjs/AnthropicTool.js +461 -0
  9. package/dist/cjs/AnthropicTool.js.map +1 -0
  10. package/dist/cjs/Generated.js +3507 -1230
  11. package/dist/cjs/Generated.js.map +1 -1
  12. package/dist/cjs/index.js +3 -1
  13. package/dist/cjs/internal/utilities.js +13 -3
  14. package/dist/cjs/internal/utilities.js.map +1 -1
  15. package/dist/dts/AnthropicClient.d.ts +673 -17
  16. package/dist/dts/AnthropicClient.d.ts.map +1 -1
  17. package/dist/dts/AnthropicLanguageModel.d.ts +217 -26
  18. package/dist/dts/AnthropicLanguageModel.d.ts.map +1 -1
  19. package/dist/dts/AnthropicTokenizer.d.ts +1 -1
  20. package/dist/dts/AnthropicTokenizer.d.ts.map +1 -1
  21. package/dist/dts/AnthropicTool.d.ts +523 -0
  22. package/dist/dts/AnthropicTool.d.ts.map +1 -0
  23. package/dist/dts/Generated.d.ts +7863 -3496
  24. package/dist/dts/Generated.d.ts.map +1 -1
  25. package/dist/dts/index.d.ts +4 -0
  26. package/dist/dts/index.d.ts.map +1 -1
  27. package/dist/esm/AnthropicClient.js +269 -188
  28. package/dist/esm/AnthropicClient.js.map +1 -1
  29. package/dist/esm/AnthropicLanguageModel.js +1022 -306
  30. package/dist/esm/AnthropicLanguageModel.js.map +1 -1
  31. package/dist/esm/AnthropicTokenizer.js +8 -6
  32. package/dist/esm/AnthropicTokenizer.js.map +1 -1
  33. package/dist/esm/AnthropicTool.js +452 -0
  34. package/dist/esm/AnthropicTool.js.map +1 -0
  35. package/dist/esm/Generated.js +3492 -1063
  36. package/dist/esm/Generated.js.map +1 -1
  37. package/dist/esm/index.js +4 -0
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/internal/utilities.js +12 -2
  40. package/dist/esm/internal/utilities.js.map +1 -1
  41. package/package.json +11 -3
  42. package/src/AnthropicClient.ts +713 -369
  43. package/src/AnthropicLanguageModel.ts +1404 -345
  44. package/src/AnthropicTokenizer.ts +14 -23
  45. package/src/AnthropicTool.ts +553 -0
  46. package/src/Generated.ts +4165 -1681
  47. package/src/index.ts +5 -0
  48. package/src/internal/utilities.ts +18 -4
@@ -1,31 +1,32 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import { AiError } from "@effect/ai/AiError"
5
- import type * as AiInput from "@effect/ai/AiInput"
6
- import * as AiLanguageModel from "@effect/ai/AiLanguageModel"
7
- import * as AiModel from "@effect/ai/AiModel"
8
- import * as AiResponse from "@effect/ai/AiResponse"
9
- import { addGenAIAnnotations } from "@effect/ai/AiTelemetry"
4
+ import * as AiError from "@effect/ai/AiError"
5
+ import * as IdGenerator from "@effect/ai/IdGenerator"
6
+ import * as LanguageModel from "@effect/ai/LanguageModel"
7
+ import * as AiModel from "@effect/ai/Model"
8
+ import type * as Prompt from "@effect/ai/Prompt"
9
+ import type * as Response from "@effect/ai/Response"
10
+ import { addGenAIAnnotations } from "@effect/ai/Telemetry"
10
11
  import type * as Tokenizer from "@effect/ai/Tokenizer"
12
+ import * as Tool from "@effect/ai/Tool"
11
13
  import * as Arr from "effect/Array"
12
14
  import * as Context from "effect/Context"
15
+ import * as DateTime from "effect/DateTime"
13
16
  import * as Effect from "effect/Effect"
14
17
  import * as Encoding from "effect/Encoding"
15
18
  import { dual } from "effect/Function"
16
19
  import * as Layer from "effect/Layer"
17
- import * as Option from "effect/Option"
18
20
  import * as Predicate from "effect/Predicate"
19
21
  import * as Stream from "effect/Stream"
20
22
  import type { Span } from "effect/Tracer"
21
23
  import type { Mutable, Simplify } from "effect/Types"
22
- import { AnthropicClient } from "./AnthropicClient.js"
24
+ import { AnthropicClient, type MessageStreamEvent } from "./AnthropicClient.js"
23
25
  import * as AnthropicTokenizer from "./AnthropicTokenizer.js"
26
+ import * as AnthropicTool from "./AnthropicTool.js"
24
27
  import type * as Generated from "./Generated.js"
25
28
  import * as InternalUtilities from "./internal/utilities.js"
26
29
 
27
- const constDisableValidation = { disableValidation: true } as const
28
-
29
30
  /**
30
31
  * @since 1.0.0
31
32
  * @category Models
@@ -70,38 +71,219 @@ export declare namespace Config {
70
71
  >
71
72
  >
72
73
  >
73
- {}
74
+ {
75
+ readonly disableParallelToolCalls?: boolean
76
+ }
74
77
  }
75
78
 
76
79
  // =============================================================================
77
- // Anthropic Provider Metadata
80
+ // Anthropic Provider Options / Metadata
78
81
  // =============================================================================
79
82
 
80
83
  /**
81
84
  * @since 1.0.0
82
- * @category Context
85
+ * @category Provider Metadata
83
86
  */
84
- export class ProviderMetadata extends Context.Tag(InternalUtilities.ProviderMetadataKey)<
85
- ProviderMetadata,
86
- ProviderMetadata.Service
87
- >() {}
87
+ export type AnthropicReasoningInfo = {
88
+ readonly type: "thinking"
89
+ /**
90
+ * Thinking content as an encrypted string, which is used to verify
91
+ * that thinking content was indeed generated by Anthropic's API.
92
+ */
93
+ readonly signature: typeof Generated.ResponseThinkingBlock.fields.thinking.Encoded
94
+ } | {
95
+ readonly type: "redacted_thinking"
96
+ /**
97
+ * Thinking content which was flagged by Anthropic's safety systems, and
98
+ * was therefore encrypted.
99
+ */
100
+ readonly redactedData: typeof Generated.RequestRedactedThinkingBlock.fields.data.Encoded
101
+ }
88
102
 
89
103
  /**
90
104
  * @since 1.0.0
105
+ * @category Provider Options
91
106
  */
92
- export declare namespace ProviderMetadata {
93
- /**
94
- * @since 1.0.0
95
- * @category Provider Metadata
96
- */
97
- export interface Service {
98
- /**
99
- * Which custom stop sequence was generated, if any.
100
- *
101
- * Will be a non-null string if one of your custom stop sequences was
102
- * generated.
103
- */
104
- readonly stopSequence?: string
107
+ declare module "@effect/ai/Prompt" {
108
+ export interface SystemMessageOptions extends ProviderOptions {
109
+ readonly anthropic?: {
110
+ /**
111
+ * A breakpoint which marks the end of reusable content eligible for caching.
112
+ */
113
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
114
+ } | undefined
115
+ }
116
+
117
+ export interface UserMessageOptions extends ProviderOptions {
118
+ readonly anthropic?: {
119
+ /**
120
+ * A breakpoint which marks the end of reusable content eligible for caching.
121
+ */
122
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
123
+ } | undefined
124
+ }
125
+
126
+ export interface AssistantMessageOptions extends ProviderOptions {
127
+ readonly anthropic?: {
128
+ /**
129
+ * A breakpoint which marks the end of reusable content eligible for caching.
130
+ */
131
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
132
+ } | undefined
133
+ }
134
+
135
+ export interface ToolMessageOptions extends ProviderOptions {
136
+ readonly anthropic?: {
137
+ /**
138
+ * A breakpoint which marks the end of reusable content eligible for caching.
139
+ */
140
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
141
+ } | undefined
142
+ }
143
+
144
+ export interface TextPartOptions extends ProviderOptions {
145
+ readonly anthropic?: {
146
+ /**
147
+ * A breakpoint which marks the end of reusable content eligible for caching.
148
+ */
149
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
150
+ } | undefined
151
+ }
152
+
153
+ export interface ReasoningPartOptions extends ProviderOptions {
154
+ readonly anthropic?:
155
+ | Simplify<
156
+ AnthropicReasoningInfo & {
157
+ /**
158
+ * A breakpoint which marks the end of reusable content eligible for caching.
159
+ */
160
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
161
+ }
162
+ >
163
+ | undefined
164
+ }
165
+
166
+ export interface FilePartOptions extends ProviderOptions {
167
+ readonly anthropic?: {
168
+ /**
169
+ * A breakpoint which marks the end of reusable content eligible for caching.
170
+ */
171
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
172
+ /**
173
+ * Whether or not citations should be enabled for the file part.
174
+ */
175
+ readonly citations?: typeof Generated.RequestCitationsConfig.Encoded | undefined
176
+ /**
177
+ * A custom title to provide to the document. If omitted, the file part's
178
+ * `fileName` property will be used.
179
+ */
180
+ readonly documentTitle?: string | undefined
181
+ /**
182
+ * Additional context about the document that will be forwarded to the
183
+ * large language model, but will not be used towards cited content.
184
+ *
185
+ * Useful for storing additional document metadata as text or stringified JSON.
186
+ */
187
+ readonly documentContext?: string | undefined
188
+ } | undefined
189
+ }
190
+
191
+ export interface ToolCallPartOptions extends ProviderOptions {
192
+ readonly anthropic?: {
193
+ /**
194
+ * A breakpoint which marks the end of reusable content eligible for caching.
195
+ */
196
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
197
+ } | undefined
198
+ }
199
+
200
+ export interface ToolResultPartOptions extends ProviderOptions {
201
+ readonly anthropic?: {
202
+ /**
203
+ * A breakpoint which marks the end of reusable content eligible for caching.
204
+ */
205
+ readonly cacheControl?: typeof Generated.CacheControlEphemeral.Encoded | undefined
206
+ } | undefined
207
+ }
208
+ }
209
+
210
+ declare module "@effect/ai/Response" {
211
+ export interface ReasoningPartMetadata extends ProviderMetadata {
212
+ readonly anthropic?: AnthropicReasoningInfo | undefined
213
+ }
214
+
215
+ export interface ReasoningStartPartMetadata extends ProviderMetadata {
216
+ readonly anthropic?: AnthropicReasoningInfo | undefined
217
+ }
218
+
219
+ export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
220
+ readonly anthropic?: AnthropicReasoningInfo | undefined
221
+ }
222
+
223
+ export interface FinishPartMetadata extends ProviderMetadata {
224
+ readonly anthropic?: {
225
+ /**
226
+ * Additional usage information provided by the Anthropic API.
227
+ */
228
+ readonly usage?: Generated.BetaUsage | undefined
229
+ /**
230
+ * Which custom stop sequence was generated, if any.
231
+ *
232
+ * If one of the custom user-defined stop sequences was generated, the
233
+ * value will be a `string` with that stop sequence.
234
+ */
235
+ readonly stopSequence?: string | undefined
236
+ } | undefined
237
+ }
238
+
239
+ export interface DocumentSourcePartMetadata extends ProviderMetadata {
240
+ readonly anthropic?: {
241
+ readonly source: "document"
242
+ readonly type: "char_location"
243
+ /**
244
+ * The text that was cited in the response.
245
+ */
246
+ readonly citedText: string
247
+ /**
248
+ * The 0-indexed starting position of the characters that were cited.
249
+ */
250
+ readonly startCharIndex: number
251
+ /**
252
+ * The exclusive ending position of the characters that were cited.
253
+ */
254
+ readonly endCharIndex: number
255
+ } | {
256
+ readonly source: "document"
257
+ readonly type: "page_location"
258
+ /**
259
+ * The text that was cited in the response.
260
+ */
261
+ readonly citedText: string
262
+ /**
263
+ * The 1-indexed starting page of pages that were cited.
264
+ */
265
+ readonly startPageNumber: number
266
+ /**
267
+ * The exclusive ending position of the pages that were cited.
268
+ */
269
+ readonly endPageNumber: number
270
+ } | undefined
271
+ }
272
+
273
+ export interface UrlSourcePartMetadata extends ProviderMetadata {
274
+ readonly anthropic?: {
275
+ readonly source: "url"
276
+ /**
277
+ * Up to 150 characters of the text content that was referenced from the
278
+ * URL source material.
279
+ */
280
+ readonly citedText: string
281
+ /**
282
+ * An internal reference that must be passed back to the Anthropic API
283
+ * during multi-turn conversations.
284
+ */
285
+ readonly encryptedIndex: string
286
+ } | undefined
105
287
  }
106
288
  }
107
289
 
@@ -111,22 +293,23 @@ export declare namespace ProviderMetadata {
111
293
 
112
294
  /**
113
295
  * @since 1.0.0
114
- * @category AiModels
296
+ * @category Ai Models
115
297
  */
116
298
  export const model = (
117
299
  model: (string & {}) | Model,
118
300
  config?: Omit<Config.Service, "model">
119
- ): AiModel.AiModel<AiLanguageModel.AiLanguageModel, AnthropicClient> => AiModel.make(layer({ model, config }))
301
+ ): AiModel.Model<"anthropic", LanguageModel.LanguageModel, AnthropicClient> =>
302
+ AiModel.make("anthropic", layer({ model, config }))
120
303
 
121
304
  /**
122
305
  * @since 1.0.0
123
- * @category AiModels
306
+ * @category Ai Models
124
307
  */
125
308
  export const modelWithTokenizer = (
126
309
  model: (string & {}) | Model,
127
310
  config?: Omit<Config.Service, "model">
128
- ): AiModel.AiModel<AiLanguageModel.AiLanguageModel | Tokenizer.Tokenizer, AnthropicClient> =>
129
- AiModel.make(layerWithTokenizer({ model, config }))
311
+ ): AiModel.Model<"anthropic", LanguageModel.LanguageModel | Tokenizer.Tokenizer, AnthropicClient> =>
312
+ AiModel.make("anthropic", layerWithTokenizer({ model, config }))
130
313
 
131
314
  /**
132
315
  * @since 1.0.0
@@ -139,80 +322,71 @@ export const make = Effect.fnUntraced(function*(options: {
139
322
  const client = yield* AnthropicClient
140
323
 
141
324
  const makeRequest = Effect.fnUntraced(
142
- function*(method: string, { prompt, system, toolChoice, tools }: AiLanguageModel.AiLanguageModelOptions) {
325
+ function*(providerOptions: LanguageModel.ProviderOptions) {
143
326
  const context = yield* Effect.context<never>()
144
- const useStructured = tools.length === 1 && tools[0].structured
145
- let tool_choice: typeof Generated.ToolChoice.Encoded | undefined = undefined
146
- if (useStructured) {
147
- tool_choice = { type: "tool", name: tools[0].name }
148
- } else if (tools.length > 0) {
149
- if (toolChoice === "required") {
150
- tool_choice = { type: "any" }
151
- } else if (typeof toolChoice === "object") {
152
- tool_choice = { type: "tool", name: toolChoice.tool }
153
- } else {
154
- tool_choice = { type: toolChoice }
155
- }
156
- }
157
- const messages = yield* makeMessages(method, prompt)
158
- return {
159
- model: options.model,
327
+ const config = { model: options.model, ...options.config, ...context.unsafeMap.get(Config.key) }
328
+ const { betas: messageBetas, messages, system } = yield* prepareMessages(providerOptions)
329
+ const { betas: toolBetas, toolChoice, tools } = yield* prepareTools(providerOptions, config)
330
+ const responseFormat = providerOptions.responseFormat
331
+ const request: typeof Generated.BetaCreateMessageParams.Encoded = {
160
332
  max_tokens: 4096,
161
- ...options.config,
162
- ...context.unsafeMap.get(Config.key),
163
- // TODO: re-evaluate a better way to do this
164
- system: Option.getOrUndefined(system),
333
+ ...config,
334
+ system,
165
335
  messages,
166
- tools: tools.length === 0 ? undefined : tools.map((tool) => ({
167
- name: tool.name,
168
- description: tool.description,
169
- input_schema: tool.parameters as any
170
- })),
171
- tool_choice
172
- } satisfies typeof Generated.CreateMessageParams.Encoded
336
+ tools: responseFormat.type === "text"
337
+ ? tools
338
+ : [{
339
+ name: responseFormat.objectName,
340
+ description: Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? "Respond with a JSON object",
341
+ input_schema: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast) as any
342
+ }],
343
+ tool_choice: responseFormat.type === "text"
344
+ ? toolChoice
345
+ : {
346
+ type: "tool",
347
+ name: responseFormat.objectName,
348
+ disable_parallel_tool_use: true
349
+ }
350
+ }
351
+ return { betas: new Set([...messageBetas, ...toolBetas]), request }
173
352
  }
174
353
  )
175
354
 
176
- return yield* AiLanguageModel.make({
355
+ return yield* LanguageModel.make({
177
356
  generateText: Effect.fnUntraced(
178
357
  function*(options) {
179
- const request = yield* makeRequest("generateText", options)
358
+ const { betas, request } = yield* makeRequest(options)
180
359
  annotateRequest(options.span, request)
181
- const rawResponse = yield* client.client.messagesPost({ params: {}, payload: request })
182
- annotateChatResponse(options.span, rawResponse)
183
- const response = yield* makeResponse(rawResponse)
184
- return response
185
- },
186
- Effect.catchAll((cause) =>
187
- AiError.is(cause) ? cause : new AiError({
188
- module: "AnthropicLanguageModel",
189
- method: "generateText",
190
- description: "An error occurred",
191
- cause
360
+ const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined
361
+ const rawResponse = yield* client.createMessage({
362
+ params: { "anthropic-beta": anthropicBeta },
363
+ payload: request
192
364
  })
193
- )
365
+ annotateResponse(options.span, rawResponse)
366
+ return yield* makeResponse(rawResponse, options)
367
+ }
194
368
  ),
195
- streamText(options) {
196
- return makeRequest("streamText", options).pipe(
197
- Effect.tap((request) => annotateRequest(options.span, request)),
198
- Effect.map(client.stream),
199
- Stream.unwrap,
200
- Stream.map((response) => {
201
- annotateStreamResponse(options.span, response)
202
- return response
203
- }),
204
- Stream.catchAll((cause) =>
205
- AiError.is(cause) ? Effect.fail(cause) : Effect.fail(
206
- new AiError({
207
- module: "AnthropicLanguageModel",
208
- method: "streamText",
209
- description: "An error occurred",
210
- cause
211
- })
212
- )
369
+
370
+ streamText: Effect.fnUntraced(
371
+ function*(options) {
372
+ const { betas, request } = yield* makeRequest(options)
373
+ annotateRequest(options.span, request)
374
+ const anthropicBeta = betas.size > 0 ? Array.from(betas).join(",") : undefined
375
+ return client.createMessageStream({
376
+ params: { "anthropic-beta": anthropicBeta },
377
+ payload: request
378
+ })
379
+ },
380
+ (effect, options) =>
381
+ effect.pipe(
382
+ Effect.flatMap((stream) => makeStreamResponse(stream, options)),
383
+ Stream.unwrap,
384
+ Stream.map((response) => {
385
+ annotateStreamResponse(options.span, response)
386
+ return response
387
+ })
213
388
  )
214
- )
215
- }
389
+ )
216
390
  })
217
391
  })
218
392
 
@@ -223,8 +397,8 @@ export const make = Effect.fnUntraced(function*(options: {
223
397
  export const layer = (options: {
224
398
  readonly model: (string & {}) | Model
225
399
  readonly config?: Omit<Config.Service, "model">
226
- }): Layer.Layer<AiLanguageModel.AiLanguageModel, never, AnthropicClient> =>
227
- Layer.effect(AiLanguageModel.AiLanguageModel, make({ model: options.model, config: options.config }))
400
+ }): Layer.Layer<LanguageModel.LanguageModel, never, AnthropicClient> =>
401
+ Layer.effect(LanguageModel.LanguageModel, make({ model: options.model, config: options.config }))
228
402
 
229
403
  /**
230
404
  * @since 1.0.0
@@ -233,7 +407,7 @@ export const layer = (options: {
233
407
  export const layerWithTokenizer = (options: {
234
408
  readonly model: (string & {}) | Model
235
409
  readonly config?: Omit<Config.Service, "model">
236
- }): Layer.Layer<AiLanguageModel.AiLanguageModel | Tokenizer.Tokenizer, never, AnthropicClient> =>
410
+ }): Layer.Layer<LanguageModel.LanguageModel | Tokenizer.Tokenizer, never, AnthropicClient> =>
237
411
  Layer.merge(layer(options), AnthropicTokenizer.layer)
238
412
 
239
413
  /**
@@ -269,282 +443,797 @@ export const withConfigOverride: {
269
443
  ))
270
444
 
271
445
  // =============================================================================
272
- // Utilities
446
+ // Prompt Conversion
273
447
  // =============================================================================
274
448
 
275
- type MessageGroup = AssistantMessageGroup | UserMessageGroup
449
+ const prepareMessages: (options: LanguageModel.ProviderOptions) => Effect.Effect<{
450
+ readonly betas: ReadonlySet<string>
451
+ readonly system: ReadonlyArray<typeof Generated.BetaRequestTextBlock.Encoded> | undefined
452
+ readonly messages: ReadonlyArray<typeof Generated.BetaInputMessage.Encoded>
453
+ }, AiError.AiError> = Effect.fnUntraced(function*(options) {
454
+ const betas = new Set<string>()
455
+ const groups = groupMessages(options.prompt)
276
456
 
277
- interface AssistantMessageGroup {
278
- readonly type: "assistant"
279
- readonly messages: Array<AiInput.AssistantMessage>
280
- }
457
+ let system: Array<typeof Generated.BetaRequestTextBlock.Encoded> | undefined = undefined
458
+ const messages: Array<typeof Generated.BetaInputMessage.Encoded> = []
281
459
 
282
- interface UserMessageGroup {
283
- readonly type: "user"
284
- readonly messages: Array<AiInput.ToolMessage | AiInput.UserMessage>
285
- }
460
+ for (let i = 0; i < groups.length; i++) {
461
+ const group = groups[i]
462
+ const isLastGroup = i === groups.length - 1
286
463
 
287
- const groupMessages = (prompt: AiInput.AiInput): Array<MessageGroup> => {
288
- const messages: Array<MessageGroup> = []
289
- let current: MessageGroup | undefined = undefined
290
- for (const message of prompt.messages) {
291
- switch (message._tag) {
292
- case "AssistantMessage": {
293
- if (current?.type !== "assistant") {
294
- current = { type: "assistant", messages: [] }
295
- messages.push(current)
296
- }
297
- current.messages.push(message)
464
+ switch (group.type) {
465
+ case "system": {
466
+ system = group.messages.map((message) => ({
467
+ type: "text",
468
+ text: message.content,
469
+ cache_control: getCacheControl(message)
470
+ }))
298
471
  break
299
472
  }
300
- case "ToolMessage":
301
- case "UserMessage": {
302
- if (current?.type !== "user") {
303
- current = { type: "user", messages: [] }
304
- messages.push(current)
305
- }
306
- current.messages.push(message)
307
- break
308
- }
309
- }
310
- }
311
- return messages
312
- }
313
473
 
314
- const makeMessages = Effect.fnUntraced(
315
- function*(method: string, prompt: AiInput.AiInput) {
316
- const messages: Array<typeof Generated.InputMessage.Encoded> = []
317
- const groups = groupMessages(prompt)
318
- for (let i = 0; i < groups.length; i++) {
319
- const group = groups[i]
320
- const isLastGroup = i === groups.length - 1
321
- switch (group.type) {
322
- case "assistant": {
323
- const content: Array<typeof Generated.InputContentBlock.Encoded> = []
324
- for (let j = 0; j < group.messages.length; j++) {
325
- const message = group.messages[j]
326
- const isLastMessage = j === group.messages.length - 1
327
- for (let k = 0; k < message.parts.length; k++) {
328
- const part = message.parts[k]
329
- const isLastPart = k === message.parts.length - 1
330
- switch (part._tag) {
331
- case "ReasoningPart": {
332
- content.push({
333
- type: "thinking",
334
- thinking: part.reasoningText,
335
- signature: part.signature!
336
- })
337
- break
338
- }
339
- case "RedactedReasoningPart": {
340
- content.push({
341
- type: "redacted_thinking",
342
- data: part.redactedText
343
- })
344
- break
345
- }
346
- case "TextPart": {
347
- content.push({
348
- type: "text",
349
- text:
350
- // Anthropic does not allow trailing whitespace in assistant
351
- // content blocks
352
- isLastGroup && isLastMessage && isLastPart
353
- ? part.text.trim()
354
- : part.text
355
- })
356
- break
357
- }
358
- case "ToolCallPart": {
359
- content.push({
360
- type: "tool_use",
361
- id: part.id,
362
- name: part.name,
363
- input: part.params as any
364
- })
365
- break
366
- }
367
- }
368
- }
369
- }
370
- messages.push({ role: "assistant", content })
371
- break
372
- }
373
- case "user": {
374
- const content: Array<typeof Generated.InputContentBlock.Encoded> = []
375
- for (let j = 0; j < group.messages.length; j++) {
376
- const message = group.messages[j]
377
- switch (message._tag) {
378
- case "ToolMessage": {
379
- for (let k = 0; k < message.parts.length; k++) {
380
- const part = message.parts[k]
381
- // TODO: support advanced tool result content parts
382
- content.push({
383
- type: "tool_result",
384
- tool_use_id: part.id,
385
- content: JSON.stringify(part.result)
386
- })
387
- }
388
- break
389
- }
390
- case "UserMessage": {
391
- for (let k = 0; k < message.parts.length; k++) {
392
- const part = message.parts[k]
393
- switch (part._tag) {
394
- case "FilePart": {
395
- if (Predicate.isUndefined(part.mediaType) || part.mediaType !== "application/pdf") {
396
- return yield* new AiError({
397
- module: "AnthropicLanguageModel",
398
- method,
399
- description: "AnthropicLanguageModel only supports PDF file inputs"
400
- })
401
- }
474
+ case "user": {
475
+ const content: Array<typeof Generated.BetaInputContentBlock.Encoded> = []
476
+
477
+ for (const message of group.messages) {
478
+ switch (message.role) {
479
+ case "user": {
480
+ for (let j = 0; j < message.content.length; j++) {
481
+ const part = message.content[j]
482
+ const isLastPart = j === message.content.length - 1
483
+
484
+ // Attempt to get the cache control from the part first. If
485
+ // the part does not have cache control defined and we are
486
+ // evaluating the last part for this message, also check the
487
+ // message for cache control.
488
+ const cacheControl = getCacheControl(part) ?? (
489
+ isLastPart ? getCacheControl(message) : undefined
490
+ )
491
+
492
+ switch (part.type) {
493
+ case "text": {
494
+ content.push({
495
+ type: "text",
496
+ text: part.text,
497
+ cache_control: cacheControl
498
+ })
499
+ break
500
+ }
501
+
502
+ case "file": {
503
+ if (part.mediaType.startsWith("image/")) {
504
+ const source = part.data instanceof URL ?
505
+ {
506
+ type: "url",
507
+ url: part.data.toString()
508
+ } as const :
509
+ {
510
+ type: "base64",
511
+ media_type: part.mediaType === "image/*"
512
+ ? "image/jpeg"
513
+ : (part.mediaType as typeof Generated.Base64ImageSourceMediaType.Encoded),
514
+ data: typeof part.data === "string" ? part.data : Encoding.encodeBase64(part.data)
515
+ } as const
516
+
402
517
  content.push({
403
- type: "document",
404
- source: {
518
+ type: "image",
519
+ source,
520
+ cache_control: cacheControl
521
+ })
522
+ } else if (part.mediaType === "application/pdf" || part.mediaType === "text/plain") {
523
+ if (part.mediaType === "application/pdf") {
524
+ betas.add("pdfs-2024-09-25")
525
+ }
526
+
527
+ const enableCitations = shouldEnableCitations(part)
528
+ const documentOptions = getDocumentMetadata(part)
529
+
530
+ const source = part.data instanceof URL
531
+ ? {
532
+ type: "url",
533
+ url: part.data.toString()
534
+ } as const
535
+ : part.mediaType === "application/pdf"
536
+ ? {
405
537
  type: "base64",
406
538
  media_type: "application/pdf",
407
- data: Encoding.encodeBase64(part.data)
408
- }
409
- })
410
- break
411
- }
412
- case "FileUrlPart": {
539
+ data: typeof part.data === "string"
540
+ ? part.data
541
+ : Encoding.encodeBase64(part.data)
542
+ } as const
543
+ : {
544
+ type: "text",
545
+ media_type: "text/plain",
546
+ data: typeof part.data === "string"
547
+ ? part.data
548
+ : Encoding.encodeBase64(part.data)
549
+ } as const
550
+
413
551
  content.push({
414
552
  type: "document",
415
- source: {
416
- type: "url",
417
- url: part.url.toString()
418
- }
419
- })
420
- break
421
- }
422
- case "TextPart": {
423
- content.push({
424
- type: "text",
425
- text: part.text
553
+ source,
554
+ title: documentOptions?.title ?? part.fileName,
555
+ ...(documentOptions?.context ? { context: documentOptions.context } : undefined),
556
+ ...(enableCitations ? { citations: { enabled: true } } : undefined),
557
+ cache_control: cacheControl
426
558
  })
427
- break
428
- }
429
- case "ImagePart": {
430
- content.push({
431
- type: "image",
432
- source: {
433
- type: "base64",
434
- media_type: part.mediaType ?? "image/jpeg" as any,
435
- data: Encoding.encodeBase64(part.data)
436
- }
437
- })
438
- break
439
- }
440
- case "ImageUrlPart": {
441
- content.push({
442
- type: "image",
443
- source: {
444
- type: "url",
445
- url: part.url.toString()
446
- }
559
+ } else {
560
+ return yield* new AiError.MalformedInput({
561
+ module: "AnthropicLanguageModel",
562
+ method: "prepareMessages",
563
+ description: `Detected unsupported media type for file: '${part.mediaType}'`
447
564
  })
448
- break
449
565
  }
566
+ break
567
+ }
568
+ }
569
+ }
570
+
571
+ break
572
+ }
573
+
574
+ // TODO: advanced tool result content parts
575
+ case "tool": {
576
+ for (const part of message.content) {
577
+ content.push({
578
+ type: "tool_result",
579
+ tool_use_id: part.id,
580
+ content: JSON.stringify(part.result)
581
+ })
582
+ }
583
+
584
+ break
585
+ }
586
+ }
587
+ }
588
+
589
+ messages.push({ role: "user", content })
590
+
591
+ break
592
+ }
593
+
594
+ case "assistant": {
595
+ const content: Array<typeof Generated.BetaInputContentBlock.Encoded> = []
596
+
597
+ for (let j = 0; j < group.messages.length; j++) {
598
+ const message = group.messages[j]
599
+ const isLastMessage = j === group.messages.length - 1
600
+
601
+ for (let k = 0; k < message.content.length; k++) {
602
+ const part = message.content[k]
603
+ const isLastPart = k === message.content.length - 1
604
+
605
+ // Attempt to get the cache control from the part first. If
606
+ // the part does not have cache control defined and we are
607
+ // evaluating the last part for this message, also check the
608
+ // message for cache control.
609
+ const cacheControl = getCacheControl(part) ?? (
610
+ isLastPart ? getCacheControl(message) : undefined
611
+ )
612
+
613
+ switch (part.type) {
614
+ case "text": {
615
+ content.push({
616
+ type: "text",
617
+ // Anthropic does not allow trailing whitespace in assistant
618
+ // content blocks
619
+ text: isLastGroup && isLastMessage && isLastPart
620
+ ? part.text.trim()
621
+ : part.text
622
+ })
623
+ break
624
+ }
625
+
626
+ case "reasoning": {
627
+ const options = part.options.anthropic
628
+ if (Predicate.isNotUndefined(options)) {
629
+ if (options.type === "thinking") {
630
+ content.push({
631
+ type: "thinking",
632
+ thinking: part.text,
633
+ signature: options.signature
634
+ })
635
+ } else {
636
+ content.push({
637
+ type: "redacted_thinking",
638
+ data: options.redactedData
639
+ })
450
640
  }
451
641
  }
452
642
  break
453
643
  }
644
+
645
+ case "tool-call": {
646
+ if (part.providerExecuted) {
647
+ if (part.name === "AnthropicWebSearch") {
648
+ content.push({
649
+ type: "server_tool_use",
650
+ id: part.id,
651
+ name: "web_search",
652
+ input: part.params as any,
653
+ cache_control: cacheControl
654
+ })
655
+ }
656
+ if (part.name === "AnthropicCodeExecution") {
657
+ content.push({
658
+ type: "server_tool_use",
659
+ id: part.id,
660
+ name: "code_execution",
661
+ input: part.params as any,
662
+ cache_control: cacheControl
663
+ })
664
+ }
665
+ } else {
666
+ content.push({
667
+ type: "tool_use",
668
+ id: part.id,
669
+ name: part.name,
670
+ input: part.params as any,
671
+ cache_control: cacheControl
672
+ })
673
+ }
674
+ break
675
+ }
454
676
  }
455
677
  }
456
- messages.push({ role: "user", content })
457
- break
458
678
  }
679
+
680
+ messages.push({ role: "assistant", content })
681
+
682
+ break
459
683
  }
460
684
  }
461
- if (Arr.isNonEmptyReadonlyArray(messages)) {
462
- return messages
463
- }
464
- return yield* new AiError({
465
- module: "AnthropicLanguageModel",
466
- method,
467
- description: "Prompt contained no messages"
468
- })
469
685
  }
470
- )
471
686
 
472
- const makeResponse = Effect.fnUntraced(
473
- function*(response: Generated.Message) {
474
- const parts: Array<AiResponse.Part> = []
475
- parts.push(
476
- new AiResponse.MetadataPart({
477
- id: response.id,
478
- model: response.model
479
- }, constDisableValidation)
480
- )
687
+ return {
688
+ system,
689
+ messages,
690
+ betas
691
+ }
692
+ })
693
+
694
+ // =============================================================================
695
+ // Response Conversion
696
+ // =============================================================================
697
+
698
+ const makeResponse: (
699
+ response: Generated.BetaMessage,
700
+ options: LanguageModel.ProviderOptions
701
+ ) => Effect.Effect<
702
+ Array<Response.PartEncoded>,
703
+ never,
704
+ IdGenerator.IdGenerator
705
+ > = Effect.fnUntraced(
706
+ function*(response, options) {
707
+ const idGenerator = yield* IdGenerator.IdGenerator
708
+ const parts: Array<Response.PartEncoded> = []
709
+ const citableDocuments = extractCitableDocuments(options.prompt)
710
+
711
+ parts.push({
712
+ type: "response-metadata",
713
+ id: response.id,
714
+ modelId: response.model,
715
+ timestamp: DateTime.formatIso(yield* DateTime.now)
716
+ })
717
+
481
718
  for (const part of response.content) {
482
719
  switch (part.type) {
483
720
  case "text": {
484
- parts.push(
485
- new AiResponse.TextPart({
721
+ // The text parts should only be added to the response here if the
722
+ // response format is `"text"`. If the response format is `"json"`,
723
+ // then the text parts must instead be added to the response when a
724
+ // tool call is received.
725
+ if (options.responseFormat.type === "text") {
726
+ parts.push({
727
+ type: "text",
486
728
  text: part.text
487
- }, constDisableValidation)
488
- )
729
+ })
730
+
731
+ if (Predicate.isNotNullable(part.citations)) {
732
+ for (const citation of part.citations) {
733
+ const source = yield* processCitation(citation, citableDocuments, idGenerator)
734
+ if (Predicate.isNotUndefined(source)) {
735
+ parts.push(source)
736
+ }
737
+ }
738
+ }
739
+ }
740
+
741
+ break
742
+ }
743
+
744
+ case "thinking": {
745
+ parts.push({
746
+ type: "reasoning",
747
+ text: part.thinking,
748
+ metadata: { anthropic: { type: "thinking", signature: part.signature } }
749
+ })
750
+ break
751
+ }
752
+
753
+ case "redacted_thinking": {
754
+ parts.push({
755
+ type: "reasoning",
756
+ text: "",
757
+ metadata: { anthropic: { type: "redacted_thinking", redactedData: part.data } }
758
+ })
489
759
  break
490
760
  }
761
+
491
762
  case "tool_use": {
492
- parts.push(
493
- AiResponse.ToolCallPart.fromUnknown({
763
+ // When a `"json"` response format is requested, the JSON that we need
764
+ // will be returned by the tool call injected into the request
765
+ if (options.responseFormat.type === "json") {
766
+ parts.push({
767
+ type: "text",
768
+ text: JSON.stringify(part.input)
769
+ })
770
+ } else {
771
+ const providerTool = AnthropicTool.getProviderDefinedToolName(part.name)
772
+ const name = Predicate.isNotUndefined(providerTool) ? providerTool : part.name
773
+ const providerName = Predicate.isNotUndefined(providerTool) ? part.name : undefined
774
+ parts.push({
775
+ type: "tool-call",
494
776
  id: part.id,
495
- name: part.name,
496
- params: part.input
777
+ name,
778
+ params: part.input,
779
+ providerName,
780
+ providerExecuted: false
497
781
  })
498
- )
782
+ }
783
+
499
784
  break
500
785
  }
501
- case "thinking": {
502
- parts.push(
503
- new AiResponse.ReasoningPart({
504
- reasoningText: part.thinking,
505
- signature: part.signature
506
- }, constDisableValidation)
507
- )
786
+
787
+ case "server_tool_use": {
788
+ const providerTool = AnthropicTool.getProviderDefinedToolName(part.name)
789
+ if (Predicate.isNotUndefined(providerTool)) {
790
+ parts.push({
791
+ type: "tool-call",
792
+ id: part.id,
793
+ name: providerTool,
794
+ params: part.input,
795
+ providerName: part.name,
796
+ providerExecuted: true
797
+ })
798
+ }
799
+
508
800
  break
509
801
  }
510
- case "redacted_thinking": {
511
- parts.push(
512
- new AiResponse.RedactedReasoningPart({
513
- redactedText: part.data
514
- }, constDisableValidation)
515
- )
802
+
803
+ case "code_execution_tool_result":
804
+ case "bash_code_execution_tool_result":
805
+ case "text_editor_code_execution_tool_result": {
806
+ parts.push({
807
+ type: "tool-result",
808
+ id: part.tool_use_id,
809
+ name: "AnthropicCodeExecution",
810
+ result: part.content,
811
+ providerName: "code_execution",
812
+ providerExecuted: true
813
+ })
814
+
815
+ break
816
+ }
817
+
818
+ case "web_search_tool_result": {
819
+ parts.push({
820
+ type: "tool-result",
821
+ id: part.tool_use_id,
822
+ name: "AnthropicWebSearch",
823
+ result: part.content,
824
+ providerName: "web_search",
825
+ providerExecuted: true
826
+ })
827
+
516
828
  break
517
829
  }
518
830
  }
519
831
  }
520
- const metadata: Mutable<ProviderMetadata.Service> = {}
521
- if (response.stop_sequence !== null) {
522
- metadata.stopSequence = response.stop_sequence
832
+
833
+ // Anthropic always returns a non-null `stop_reason` for non-streaming responses
834
+ const finishReason = InternalUtilities.resolveFinishReason(
835
+ response.stop_reason!,
836
+ options.responseFormat.type === "json"
837
+ )
838
+
839
+ parts.push({
840
+ type: "finish",
841
+ reason: finishReason,
842
+ usage: {
843
+ inputTokens: response.usage.input_tokens,
844
+ outputTokens: response.usage.output_tokens,
845
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens,
846
+ cachedInputTokens: response.usage.cache_read_input_tokens ?? undefined
847
+ },
848
+ metadata: {
849
+ anthropic: {
850
+ usage: response.usage,
851
+ stopSequence: response.stop_sequence ?? undefined
852
+ }
853
+ }
854
+ })
855
+
856
+ return parts
857
+ }
858
+ )
859
+
860
+ const makeStreamResponse: (
861
+ stream: Stream.Stream<MessageStreamEvent, AiError.AiError>,
862
+ options: LanguageModel.ProviderOptions
863
+ ) => Effect.Effect<
864
+ Stream.Stream<Response.StreamPartEncoded, AiError.AiError>,
865
+ never,
866
+ IdGenerator.IdGenerator
867
+ > = Effect.fnUntraced(
868
+ function*(stream, options) {
869
+ const idGenerator = yield* IdGenerator.IdGenerator
870
+ const citableDocuments = extractCitableDocuments(options.prompt)
871
+
872
+ // Setup all requisite state for the streaming response
873
+ let finishReason: Response.FinishReason = "unknown"
874
+ const contentBlocks: Record<
875
+ number,
876
+ | {
877
+ readonly type: "text"
878
+ }
879
+ | {
880
+ readonly type: "reasoning"
881
+ }
882
+ | {
883
+ readonly type: "tool-call"
884
+ readonly id: string
885
+ readonly name: string
886
+ params: string
887
+ readonly providerName: string | undefined
888
+ readonly providerExecuted: boolean
889
+ }
890
+ > = {}
891
+ let blockType:
892
+ | "text"
893
+ | "thinking"
894
+ | "redacted_thinking"
895
+ | "tool_use"
896
+ | "server_tool_use"
897
+ | "web_search_tool_result"
898
+ | "code_execution_tool_result"
899
+ | "bash_code_execution_tool_result"
900
+ | "text_editor_code_execution_tool_result"
901
+ | "mcp_tool_use"
902
+ | "mcp_tool_result"
903
+ | "container_upload"
904
+ | undefined = undefined
905
+ const usage: Mutable<typeof Response.Usage.Encoded> = {
906
+ inputTokens: undefined,
907
+ outputTokens: undefined,
908
+ totalTokens: undefined
523
909
  }
524
- parts.push(
525
- new AiResponse.FinishPart({
526
- // Anthropic always returns a non-null `stop_reason` for non-streaming responses
527
- reason: InternalUtilities.resolveFinishReason(response.stop_reason!),
528
- usage: new AiResponse.Usage({
529
- inputTokens: response.usage.input_tokens,
530
- outputTokens: response.usage.output_tokens,
531
- totalTokens: response.usage.input_tokens + response.usage.output_tokens,
532
- reasoningTokens: 0,
533
- cacheReadInputTokens: response.usage.cache_read_input_tokens ?? 0,
534
- cacheWriteInputTokens: response.usage.cache_creation_input_tokens ?? 0
535
- }),
536
- providerMetadata: { [InternalUtilities.ProviderMetadataKey]: metadata }
537
- }, constDisableValidation)
910
+ let metaUsage: Generated.BetaUsage | undefined = undefined
911
+ let stopSequence: string | undefined = undefined
912
+
913
+ return stream.pipe(
914
+ Stream.mapEffect(Effect.fnUntraced(function*(event) {
915
+ const parts: Array<Response.StreamPartEncoded> = []
916
+
917
+ switch (event.type) {
918
+ case "ping": {
919
+ break
920
+ }
921
+
922
+ case "message_start": {
923
+ // Track usage metadata
924
+ usage.inputTokens = event.message.usage.input_tokens
925
+ metaUsage = event.message.usage
926
+
927
+ // Track response metadata
928
+ parts.push({
929
+ type: "response-metadata",
930
+ id: event.message.id,
931
+ modelId: event.message.model,
932
+ timestamp: DateTime.formatIso(yield* DateTime.now)
933
+ })
934
+
935
+ break
936
+ }
937
+
938
+ case "message_delta": {
939
+ // Track usage metadata
940
+ if (Predicate.isNotNullable(event.usage.output_tokens)) {
941
+ usage.outputTokens = event.usage.output_tokens
942
+ }
943
+ usage.totalTokens = (usage.inputTokens ?? 0) + (event.usage.output_tokens ?? 0)
944
+
945
+ // Track stop sequence metadata
946
+ if (Predicate.isNotNullable(event.delta.stop_sequence)) {
947
+ stopSequence = event.delta.stop_sequence
948
+ }
949
+
950
+ // Track the response finish reason
951
+ if (Predicate.isNotNullable(event.delta.stop_reason)) {
952
+ finishReason = InternalUtilities.resolveFinishReason(event.delta.stop_reason)
953
+ }
954
+
955
+ break
956
+ }
957
+
958
+ case "message_stop": {
959
+ parts.push({
960
+ type: "finish",
961
+ reason: finishReason,
962
+ usage,
963
+ metadata: { anthropic: { usage: metaUsage, stopSequence } }
964
+ })
965
+
966
+ break
967
+ }
968
+
969
+ case "content_block_start": {
970
+ blockType = event.content_block.type
971
+
972
+ switch (event.content_block.type) {
973
+ case "text": {
974
+ contentBlocks[event.index] = { type: "text" }
975
+
976
+ parts.push({
977
+ type: "text-start",
978
+ id: event.index.toString()
979
+ })
980
+
981
+ break
982
+ }
983
+
984
+ case "thinking": {
985
+ contentBlocks[event.index] = { type: "reasoning" }
986
+
987
+ parts.push({
988
+ type: "reasoning-start",
989
+ id: event.index.toString()
990
+ })
991
+
992
+ break
993
+ }
994
+
995
+ case "redacted_thinking": {
996
+ contentBlocks[event.index] = { type: "reasoning" }
997
+
998
+ parts.push({
999
+ type: "reasoning-start",
1000
+ id: event.index.toString(),
1001
+ metadata: {
1002
+ anthropic: {
1003
+ type: "redacted_thinking",
1004
+ redactedData: event.content_block.data
1005
+ }
1006
+ }
1007
+ })
1008
+
1009
+ break
1010
+ }
1011
+
1012
+ case "tool_use": {
1013
+ const toolName = event.content_block.name
1014
+ const providerTool = AnthropicTool.getProviderDefinedToolName(toolName)
1015
+ const name = Predicate.isNotUndefined(providerTool) ? providerTool : toolName
1016
+ const providerName = Predicate.isNotUndefined(providerTool) ? toolName : undefined
1017
+
1018
+ contentBlocks[event.index] = {
1019
+ type: "tool-call",
1020
+ id: event.content_block.id,
1021
+ name,
1022
+ params: "",
1023
+ providerName,
1024
+ providerExecuted: false
1025
+ }
1026
+
1027
+ parts.push({
1028
+ type: "tool-params-start",
1029
+ id: event.content_block.id,
1030
+ name: toolName,
1031
+ providerName,
1032
+ providerExecuted: false
1033
+ })
1034
+
1035
+ break
1036
+ }
1037
+
1038
+ case "server_tool_use": {
1039
+ const toolName = event.content_block.name
1040
+ const providerTool = AnthropicTool.getProviderDefinedToolName(toolName)
1041
+ if (Predicate.isNotUndefined(providerTool)) {
1042
+ contentBlocks[event.index] = {
1043
+ type: "tool-call",
1044
+ id: event.content_block.id,
1045
+ name: providerTool,
1046
+ params: "",
1047
+ providerName: toolName,
1048
+ providerExecuted: true
1049
+ }
1050
+
1051
+ parts.push({
1052
+ type: "tool-params-start",
1053
+ id: event.content_block.id,
1054
+ name: providerTool,
1055
+ providerName: toolName,
1056
+ providerExecuted: true
1057
+ })
1058
+ }
1059
+
1060
+ break
1061
+ }
1062
+
1063
+ case "code_execution_tool_result":
1064
+ case "bash_code_execution_tool_result":
1065
+ case "text_editor_code_execution_tool_result": {
1066
+ parts.push({
1067
+ type: "tool-result",
1068
+ id: event.content_block.tool_use_id,
1069
+ name: "AnthropicCodeExecution",
1070
+ result: event.content_block.content,
1071
+ providerName: "code_execution",
1072
+ providerExecuted: true
1073
+ })
1074
+
1075
+ break
1076
+ }
1077
+
1078
+ case "web_search_tool_result": {
1079
+ parts.push({
1080
+ type: "tool-result",
1081
+ id: event.content_block.tool_use_id,
1082
+ name: "AnthropicWebSearch",
1083
+ result: event.content_block.content,
1084
+ providerName: "web_search",
1085
+ providerExecuted: true
1086
+ })
1087
+
1088
+ break
1089
+ }
1090
+ }
1091
+
1092
+ break
1093
+ }
1094
+
1095
+ case "content_block_delta": {
1096
+ switch (event.delta.type) {
1097
+ case "text_delta": {
1098
+ parts.push({
1099
+ type: "text-delta",
1100
+ id: event.index.toString(),
1101
+ delta: event.delta.text
1102
+ })
1103
+
1104
+ break
1105
+ }
1106
+
1107
+ case "thinking_delta": {
1108
+ parts.push({
1109
+ type: "reasoning-delta",
1110
+ id: event.index.toString(),
1111
+ delta: event.delta.thinking
1112
+ })
1113
+
1114
+ break
1115
+ }
1116
+
1117
+ case "signature_delta": {
1118
+ if (blockType === "thinking") {
1119
+ parts.push({
1120
+ type: "reasoning-delta",
1121
+ id: event.index.toString(),
1122
+ delta: "",
1123
+ metadata: {
1124
+ anthropic: {
1125
+ type: "thinking",
1126
+ signature: event.delta.signature
1127
+ }
1128
+ }
1129
+ })
1130
+ }
1131
+
1132
+ break
1133
+ }
1134
+
1135
+ case "input_json_delta": {
1136
+ const contentBlock = contentBlocks[event.index]
1137
+ const delta = event.delta.partial_json
1138
+
1139
+ if (contentBlock.type === "tool-call") {
1140
+ parts.push({
1141
+ type: "tool-params-delta",
1142
+ id: contentBlock.id,
1143
+ delta
1144
+ })
1145
+
1146
+ contentBlock.params += delta
1147
+ }
1148
+
1149
+ break
1150
+ }
1151
+
1152
+ case "citations_delta": {
1153
+ const citation = event.delta.citation
1154
+
1155
+ const source = yield* processCitation(citation, citableDocuments, idGenerator)
1156
+ if (Predicate.isNotUndefined(source)) {
1157
+ parts.push(source)
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ break
1163
+ }
1164
+
1165
+ case "content_block_stop": {
1166
+ if (Predicate.isNotNullable(contentBlocks[event.index])) {
1167
+ const contentBlock = contentBlocks[event.index]
1168
+
1169
+ switch (contentBlock.type) {
1170
+ case "text": {
1171
+ parts.push({
1172
+ type: "text-end",
1173
+ id: event.index.toString()
1174
+ })
1175
+ break
1176
+ }
1177
+
1178
+ case "reasoning": {
1179
+ parts.push({
1180
+ type: "reasoning-end",
1181
+ id: event.index.toString()
1182
+ })
1183
+ break
1184
+ }
1185
+
1186
+ case "tool-call": {
1187
+ parts.push({
1188
+ type: "tool-params-end",
1189
+ id: contentBlock.id
1190
+ })
1191
+
1192
+ // If the tool call has no parameters, an empty string is returned
1193
+ const params = contentBlock.params.length === 0 ? "{}" : contentBlock.params
1194
+
1195
+ parts.push({
1196
+ type: "tool-call",
1197
+ id: contentBlock.id,
1198
+ name: contentBlock.name,
1199
+ params: JSON.parse(params),
1200
+ providerName: contentBlock.providerName,
1201
+ providerExecuted: contentBlock.providerExecuted
1202
+ })
1203
+
1204
+ break
1205
+ }
1206
+ }
1207
+
1208
+ delete contentBlocks[event.index]
1209
+ }
1210
+
1211
+ blockType = undefined
1212
+
1213
+ break
1214
+ }
1215
+
1216
+ case "error": {
1217
+ parts.push({ type: "error", error: event.error })
1218
+
1219
+ break
1220
+ }
1221
+ }
1222
+
1223
+ return parts
1224
+ })),
1225
+ Stream.flattenIterables
538
1226
  )
539
- return new AiResponse.AiResponse({
540
- parts
541
- }, constDisableValidation)
542
1227
  }
543
1228
  )
544
1229
 
1230
+ // =============================================================================
1231
+ // Telemetry
1232
+ // =============================================================================
1233
+
545
1234
  const annotateRequest = (
546
1235
  span: Span,
547
- request: typeof Generated.CreateMessageParams.Encoded
1236
+ request: typeof Generated.BetaCreateMessageParams.Encoded
548
1237
  ): void => {
549
1238
  addGenAIAnnotations(span, {
550
1239
  system: "anthropic",
@@ -562,10 +1251,7 @@ const annotateRequest = (
562
1251
  })
563
1252
  }
564
1253
 
565
- const annotateChatResponse = (
566
- span: Span,
567
- response: typeof Generated.Message.Encoded
568
- ): void => {
1254
+ const annotateResponse = (span: Span, response: Generated.BetaMessage): void => {
569
1255
  addGenAIAnnotations(span, {
570
1256
  response: {
571
1257
  id: response.id,
@@ -579,21 +1265,394 @@ const annotateChatResponse = (
579
1265
  })
580
1266
  }
581
1267
 
582
- const annotateStreamResponse = (
583
- span: Span,
584
- response: AiResponse.AiResponse
585
- ) => {
586
- const metadataPart = response.parts.find((part) => part._tag === "MetadataPart")
587
- const finishPart = response.parts.find((part) => part._tag === "FinishPart")
588
- addGenAIAnnotations(span, {
589
- response: {
590
- id: metadataPart?.id,
591
- model: metadataPart?.model,
592
- finishReasons: finishPart?.reason ? [finishPart.reason] : undefined
593
- },
594
- usage: {
595
- inputTokens: finishPart?.usage.inputTokens,
596
- outputTokens: finishPart?.usage.outputTokens
1268
+ const annotateStreamResponse = (span: Span, part: Response.StreamPartEncoded) => {
1269
+ if (part.type === "response-metadata") {
1270
+ addGenAIAnnotations(span, {
1271
+ response: {
1272
+ id: part.id,
1273
+ model: part.modelId
1274
+ }
1275
+ })
1276
+ }
1277
+ if (part.type === "finish") {
1278
+ addGenAIAnnotations(span, {
1279
+ response: {
1280
+ finishReasons: [part.reason]
1281
+ },
1282
+ usage: {
1283
+ inputTokens: part.usage.inputTokens,
1284
+ outputTokens: part.usage.outputTokens
1285
+ }
1286
+ })
1287
+ }
1288
+ }
1289
+
1290
+ // =============================================================================
1291
+ // Tool Calling
1292
+ // =============================================================================
1293
+
1294
+ /**
1295
+ * Represents all possible Anthropic provider-defined tools.
1296
+ *
1297
+ * @since 1.0.0
1298
+ * @category Models
1299
+ */
1300
+ export type AnthropicTools =
1301
+ | typeof Generated.BetaTool.Encoded
1302
+ | typeof Generated.BetaBashTool20241022.Encoded
1303
+ | typeof Generated.BetaBashTool20250124.Encoded
1304
+ | typeof Generated.BetaComputerUseTool20241022.Encoded
1305
+ | typeof Generated.BetaComputerUseTool20250124.Encoded
1306
+ | typeof Generated.BetaTextEditor20241022.Encoded
1307
+ | typeof Generated.BetaTextEditor20250124.Encoded
1308
+ | typeof Generated.BetaTextEditor20250429.Encoded
1309
+ | typeof Generated.BetaTextEditor20250728.Encoded
1310
+
1311
+ /**
1312
+ * A helper method which takes in large language model provider options from
1313
+ * the base Effect AI SDK as well as Anthropic request configuration options
1314
+ * and returns the prepared tools, tool choice, and Anthropic betas to include
1315
+ * in a request.
1316
+ *
1317
+ * This method is primarily exposed for use by other Effect provider
1318
+ * integrations which can utilize Anthropic models (i.e. Amazon Bedrock).
1319
+ *
1320
+ * @since 1.0.0
1321
+ * @category Tool Calling
1322
+ */
1323
+ export const prepareTools: (options: LanguageModel.ProviderOptions, config: Config.Service) => Effect.Effect<{
1324
+ readonly betas: ReadonlySet<string>
1325
+ readonly tools: ReadonlyArray<AnthropicTools> | undefined
1326
+ readonly toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined
1327
+ }, AiError.AiError> = Effect.fnUntraced(function*(options, config) {
1328
+ // Return immediately if no tools are in the toolkit or a tool choice of
1329
+ // "none" was specified
1330
+ if (options.tools.length === 0 || options.toolChoice === "none") {
1331
+ return { betas: new Set(), tools: undefined, toolChoice: undefined }
1332
+ }
1333
+
1334
+ const betas = new Set<string>()
1335
+ let tools: Array<AnthropicTools> = []
1336
+ let toolChoice: typeof Generated.BetaToolChoice.Encoded | undefined = undefined
1337
+
1338
+ // Convert the tools in the toolkit to the provider-defined format
1339
+ for (const tool of options.tools) {
1340
+ if (Tool.isUserDefined(tool)) {
1341
+ tools.push({
1342
+ name: tool.name,
1343
+ description: Tool.getDescription(tool as any),
1344
+ input_schema: Tool.getJsonSchema(tool as any) as any
1345
+ })
597
1346
  }
598
- })
1347
+
1348
+ if (Tool.isProviderDefined(tool)) {
1349
+ switch (tool.id) {
1350
+ case "anthropic.bash_20241022": {
1351
+ betas.add("computer-use-2024-10-22")
1352
+ tools.push({
1353
+ name: "bash",
1354
+ type: "bash_20241022"
1355
+ })
1356
+ break
1357
+ }
1358
+ case "anthropic.bash_20250124": {
1359
+ betas.add("computer-use-2025-01-24")
1360
+ tools.push({
1361
+ name: "bash",
1362
+ type: "bash_20250124"
1363
+ })
1364
+ break
1365
+ }
1366
+ case "anthropic.code_execution_20250522": {
1367
+ betas.add("code-execution-2025-05-22")
1368
+ tools.push({
1369
+ ...tool.args,
1370
+ name: "code_execution",
1371
+ type: "code_execution_2025522"
1372
+ })
1373
+ break
1374
+ }
1375
+ case "anthropic.code_execution_20250825": {
1376
+ betas.add("code-execution-2025-08-25")
1377
+ tools.push({
1378
+ ...tool.args,
1379
+ name: "code_execution",
1380
+ type: "code_execution_20250825"
1381
+ })
1382
+ break
1383
+ }
1384
+ case "anthropic.computer_use_20241022": {
1385
+ betas.add("computer-use-2025-10-22")
1386
+ tools.push({
1387
+ ...tool.args,
1388
+ name: "computer",
1389
+ type: "computer_20241022"
1390
+ })
1391
+ break
1392
+ }
1393
+ case "anthropic.computer_use_20250124": {
1394
+ betas.add("computer-use-2025-01-24")
1395
+ tools.push({
1396
+ ...tool.args,
1397
+ name: "computer",
1398
+ type: "computer_20250124"
1399
+ })
1400
+ break
1401
+ }
1402
+ case "anthropic.text_editor_20241022": {
1403
+ betas.add("computer-use-2024-10-22")
1404
+ tools.push({
1405
+ name: "str_replace_editor",
1406
+ type: "text_editor_20241022"
1407
+ })
1408
+ break
1409
+ }
1410
+ case "anthropic.text_editor_20250124": {
1411
+ betas.add("computer-use-2025-01-24")
1412
+ tools.push({
1413
+ name: "str_replace_editor",
1414
+ type: "text_editor_20250124"
1415
+ })
1416
+ break
1417
+ }
1418
+ case "anthropic.text_editor_20250429": {
1419
+ betas.add("computer-use-2025-01-24")
1420
+ tools.push({
1421
+ name: "str_replace_based_edit_tool",
1422
+ type: "text_editor_20250429"
1423
+ })
1424
+ break
1425
+ }
1426
+ case "anthropic.text_editor_20250728": {
1427
+ tools.push({
1428
+ name: "str_replace_based_edit_tool",
1429
+ type: "text_editor_20250728"
1430
+ })
1431
+ break
1432
+ }
1433
+ case "anthropic.web_search_20250305": {
1434
+ tools.push({
1435
+ ...tool.args,
1436
+ name: "web_search",
1437
+ type: "web_search_20250305"
1438
+ })
1439
+ break
1440
+ }
1441
+ default: {
1442
+ return yield* new AiError.MalformedInput({
1443
+ module: "AnthropicLanguageModel",
1444
+ method: "prepareTools",
1445
+ description: `Received request to call unknown provider-defined tool '${tool.name}'`
1446
+ })
1447
+ }
1448
+ }
1449
+ }
1450
+ }
1451
+
1452
+ // Convert the tool choice to the provider-defined format
1453
+ if (options.toolChoice === "auto") {
1454
+ toolChoice = {
1455
+ type: "auto",
1456
+ disable_parallel_tool_use: config.disableParallelToolCalls
1457
+ }
1458
+ } else if (options.toolChoice === "required") {
1459
+ toolChoice = {
1460
+ type: "any",
1461
+ disable_parallel_tool_use: config.disableParallelToolCalls
1462
+ }
1463
+ } else if ("tool" in options.toolChoice) {
1464
+ toolChoice = {
1465
+ type: "tool",
1466
+ name: options.toolChoice.tool,
1467
+ disable_parallel_tool_use: config.disableParallelToolCalls
1468
+ }
1469
+ } else {
1470
+ const allowedTools = new Set(options.toolChoice.oneOf)
1471
+ tools = tools.filter((tool) => allowedTools.has(tool.name))
1472
+ toolChoice = {
1473
+ type: options.toolChoice.mode === "required" ? "any" : "auto",
1474
+ disable_parallel_tool_use: config.disableParallelToolCalls
1475
+ }
1476
+ }
1477
+
1478
+ return { betas, tools, toolChoice }
1479
+ })
1480
+
1481
+ // =============================================================================
1482
+ // Utilities
1483
+ // =============================================================================
1484
+
1485
+ type ContentGroup = SystemMessageGroup | AssistantMessageGroup | UserMessageGroup
1486
+
1487
+ interface SystemMessageGroup {
1488
+ readonly type: "system"
1489
+ readonly messages: Array<Prompt.SystemMessage>
599
1490
  }
1491
+
1492
+ interface AssistantMessageGroup {
1493
+ readonly type: "assistant"
1494
+ readonly messages: Array<Prompt.AssistantMessage>
1495
+ }
1496
+
1497
+ interface UserMessageGroup {
1498
+ readonly type: "user"
1499
+ readonly messages: Array<Prompt.ToolMessage | Prompt.UserMessage>
1500
+ }
1501
+
1502
+ const groupMessages = (prompt: Prompt.Prompt): Array<ContentGroup> => {
1503
+ const messages: Array<ContentGroup> = []
1504
+ let current: ContentGroup | undefined = undefined
1505
+ for (const message of prompt.content) {
1506
+ switch (message.role) {
1507
+ case "system": {
1508
+ if (current?.type !== "system") {
1509
+ current = { type: "system", messages: [] }
1510
+ messages.push(current)
1511
+ }
1512
+ current.messages.push(message)
1513
+ break
1514
+ }
1515
+ case "assistant": {
1516
+ if (current?.type !== "assistant") {
1517
+ current = { type: "assistant", messages: [] }
1518
+ messages.push(current)
1519
+ }
1520
+ current.messages.push(message)
1521
+ break
1522
+ }
1523
+ case "tool":
1524
+ case "user": {
1525
+ if (current?.type !== "user") {
1526
+ current = { type: "user", messages: [] }
1527
+ messages.push(current)
1528
+ }
1529
+ current.messages.push(message)
1530
+ break
1531
+ }
1532
+ }
1533
+ }
1534
+ return messages
1535
+ }
1536
+
1537
+ const isCitationPart = (part: Prompt.UserMessage["content"][number]): part is Prompt.FilePart => {
1538
+ if (part.type === "file" && (part.mediaType === "application/pdf" || part.mediaType === "text/plain")) {
1539
+ return part.options.anthropic?.citations?.enabled ?? false
1540
+ }
1541
+ return false
1542
+ }
1543
+
1544
+ interface CitableDocument {
1545
+ readonly title: string
1546
+ readonly fileName: string | undefined
1547
+ readonly mediaType: string
1548
+ }
1549
+
1550
+ const extractCitableDocuments = (prompt: Prompt.Prompt): ReadonlyArray<CitableDocument> => {
1551
+ const citableDocuments: Array<CitableDocument> = []
1552
+ for (const message of prompt.content) {
1553
+ if (message.role === "user") {
1554
+ for (const part of message.content) {
1555
+ if (isCitationPart(part)) {
1556
+ citableDocuments.push({
1557
+ title: part.fileName ?? "Untitled Document",
1558
+ fileName: part.fileName,
1559
+ mediaType: part.mediaType
1560
+ })
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1565
+ return citableDocuments
1566
+ }
1567
+
1568
+ const getCacheControl = (
1569
+ part:
1570
+ | Prompt.SystemMessage
1571
+ | Prompt.UserMessage
1572
+ | Prompt.AssistantMessage
1573
+ | Prompt.ToolMessage
1574
+ | Prompt.UserMessagePart
1575
+ | Prompt.AssistantMessagePart
1576
+ | Prompt.ToolMessagePart
1577
+ ): typeof Generated.CacheControlEphemeral.Encoded | undefined => part.options.anthropic?.cacheControl
1578
+
1579
+ const getDocumentMetadata = (part: Prompt.FilePart): {
1580
+ readonly title: string | undefined
1581
+ readonly context: string | undefined
1582
+ } | undefined => {
1583
+ const options = part.options.anthropic
1584
+ if (Predicate.isNotUndefined(options)) {
1585
+ return {
1586
+ title: options.documentTitle,
1587
+ context: options.documentContext
1588
+ }
1589
+ }
1590
+ return undefined
1591
+ }
1592
+
1593
+ const shouldEnableCitations = (part: Prompt.FilePart): boolean => part.options.anthropic?.citations?.enabled ?? false
1594
+
1595
+ const processCitation: (
1596
+ citation:
1597
+ | Generated.ResponseCharLocationCitation
1598
+ | Generated.ResponsePageLocationCitation
1599
+ | Generated.ResponseContentBlockLocationCitation
1600
+ | Generated.ResponseWebSearchResultLocationCitation
1601
+ | Generated.ResponseSearchResultLocationCitation,
1602
+ citableDocuments: ReadonlyArray<CitableDocument>,
1603
+ idGenerator: IdGenerator.Service
1604
+ ) => Effect.Effect<Response.DocumentSourcePartEncoded | Response.UrlSourcePartEncoded | undefined> = Effect.fnUntraced(
1605
+ function*(citation, citableDocuments, idGenerator) {
1606
+ if (citation.type === "page_location" || citation.type === "char_location") {
1607
+ const citedDocument = citableDocuments[citation.document_index]
1608
+ if (Predicate.isNotUndefined(citedDocument)) {
1609
+ const id = yield* idGenerator.generateId()
1610
+
1611
+ const metadata = citation.type === "char_location"
1612
+ ? {
1613
+ source: "document",
1614
+ type: citation.type,
1615
+ citedText: citation.cited_text,
1616
+ startCharIndex: citation.start_char_index,
1617
+ endCharIndex: citation.end_char_index
1618
+ } as const
1619
+ : {
1620
+ source: "document",
1621
+ type: citation.type,
1622
+ citedText: citation.cited_text,
1623
+ startPageNumber: citation.start_page_number,
1624
+ endPageNumber: citation.end_page_number
1625
+ } as const
1626
+
1627
+ return {
1628
+ type: "source",
1629
+ sourceType: "document",
1630
+ id,
1631
+ mediaType: citedDocument.mediaType,
1632
+ title: citation.document_title ?? citedDocument.title,
1633
+ fileName: citedDocument.fileName,
1634
+ metadata: { anthropic: metadata }
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ if (citation.type === "web_search_result_location") {
1640
+ const id = yield* idGenerator.generateId()
1641
+
1642
+ const metadata = {
1643
+ source: "url",
1644
+ citedText: citation.cited_text,
1645
+ encryptedIndex: citation.encrypted_index
1646
+ } as const
1647
+
1648
+ return {
1649
+ type: "source",
1650
+ sourceType: "url",
1651
+ id,
1652
+ url: citation.url,
1653
+ title: citation.title ?? "Untitled",
1654
+ metadata: { anthropic: metadata }
1655
+ }
1656
+ }
1657
+ }
1658
+ )