@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
@@ -2,28 +2,25 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
  import * as AiError from "@effect/ai/AiError"
5
- import * as AiInput from "@effect/ai/AiInput"
6
- import * as AiResponse from "@effect/ai/AiResponse"
7
5
  import * as Sse from "@effect/experimental/Sse"
6
+ import * as Headers from "@effect/platform/Headers"
8
7
  import * as HttpBody from "@effect/platform/HttpBody"
9
8
  import * as HttpClient from "@effect/platform/HttpClient"
10
- import type * as HttpClientError from "@effect/platform/HttpClientError"
11
9
  import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
10
+ import * as Arr from "effect/Array"
11
+ import * as Chunk from "effect/Chunk"
12
12
  import * as Config from "effect/Config"
13
13
  import type { ConfigError } from "effect/ConfigError"
14
14
  import * as Context from "effect/Context"
15
15
  import * as Effect from "effect/Effect"
16
16
  import { identity } from "effect/Function"
17
17
  import * as Layer from "effect/Layer"
18
- import * as Option from "effect/Option"
19
- import * as Predicate from "effect/Predicate"
20
18
  import * as Redacted from "effect/Redacted"
19
+ import * as Schema from "effect/Schema"
20
+ import type * as Scope from "effect/Scope"
21
21
  import * as Stream from "effect/Stream"
22
22
  import { AnthropicConfig } from "./AnthropicConfig.js"
23
23
  import * as Generated from "./Generated.js"
24
- import * as InternalUtilities from "./internal/utilities.js"
25
-
26
- const constDisableValidation = { disableValidation: true } as const
27
24
 
28
25
  /**
29
26
  * @since 1.0.0
@@ -31,285 +28,678 @@ const constDisableValidation = { disableValidation: true } as const
31
28
  */
32
29
  export class AnthropicClient extends Context.Tag(
33
30
  "@effect/ai-anthropic/AnthropicClient"
34
- )<AnthropicClient, AnthropicClient.Service>() {}
31
+ )<AnthropicClient, Service>() {}
35
32
 
36
33
  /**
34
+ * Represents the interface that the `AnthropicClient` service provides.
35
+ *
36
+ * This service abstracts the complexity of communicating with Anthropic's API,
37
+ * providing both high-level text generation methods and low-level HTTP access
38
+ * for advanced use cases.
39
+ *
37
40
  * @since 1.0.0
41
+ * @category Models
38
42
  */
39
- export declare namespace AnthropicClient {
43
+ export interface Service {
40
44
  /**
41
- * @since 1.0.0
42
- * @category Models
45
+ * The underlying HTTP client capable of communicating with the Anthropic API.
46
+ *
47
+ * This client is pre-configured with authentication, base URL, and standard
48
+ * headers required for Anthropic API communication. It provides direct access
49
+ * to the generated Anthropic API client for operations not covered by the
50
+ * higher-level methods.
51
+ *
52
+ * Use this when you need to:
53
+ * - Access provider-specific API endpoints not available through the AI SDK
54
+ * - Implement custom request/response handling
55
+ * - Use Anthropic API features not yet supported by the Effect AI abstractions
56
+ * - Perform batch operations or non-streaming requests
57
+ *
58
+ * The client automatically handles authentication and follows Anthropic's
59
+ * API conventions for request formatting and error handling.
43
60
  */
44
- export interface Service {
45
- readonly client: Generated.Client
46
- readonly streamRequest: <A>(
47
- request: HttpClientRequest.HttpClientRequest
48
- ) => Stream.Stream<A, HttpClientError.HttpClientError>
49
- readonly stream: (
50
- request: StreamCompletionRequest
51
- ) => Stream.Stream<AiResponse.AiResponse, HttpClientError.HttpClientError>
52
- }
61
+ readonly client: Generated.Client
62
+
63
+ readonly streamRequest: <A, I, R>(
64
+ request: HttpClientRequest.HttpClientRequest,
65
+ schema: Schema.Schema<A, I, R>
66
+ ) => Stream.Stream<A, AiError.AiError, R>
67
+
68
+ readonly createMessage: (options: {
69
+ readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined
70
+ readonly payload: typeof Generated.BetaCreateMessageParams.Encoded
71
+ }) => Effect.Effect<Generated.BetaMessage, AiError.AiError>
72
+
73
+ readonly createMessageStream: (options: {
74
+ readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined
75
+ readonly payload: Omit<typeof Generated.BetaCreateMessageParams.Encoded, "stream">
76
+ }) => Stream.Stream<MessageStreamEvent, AiError.AiError>
53
77
  }
54
78
 
55
79
  /**
56
80
  * @since 1.0.0
57
81
  * @category Constructors
58
82
  */
59
- export const make = (options: {
83
+ export const make: (options: {
84
+ /**
85
+ * The API key that will be used to authenticate with Anthropic's API.
86
+ *
87
+ * The key is wrapped in a `Redacted` type to prevent accidental logging or
88
+ * exposure in debugging output, helping maintain security best practices.
89
+ *
90
+ * The key is automatically included in the `x-api-key` header for all API
91
+ * requests made through this client, which is automatically redacted in logs
92
+ * output by Effect loggers.
93
+ *
94
+ * Leave `undefined` if authentication will be handled through other means
95
+ * (e.g., environment-based authentication, proxy authentication, or when
96
+ * using a mock server that doesn't require authentication).
97
+ */
60
98
  readonly apiKey?: Redacted.Redacted | undefined
99
+
100
+ /**
101
+ * The base URL endpoint used to communicate with Anthropic's API.
102
+ *
103
+ * This property determines the HTTP destination for all API requests made by
104
+ * this client.
105
+ *
106
+ * Defaults to `"https://api.anthropic.com"`.
107
+ *
108
+ * Override this value when you need to:
109
+ * - Point to a different Anthropic environment (e.g., staging or sandbox
110
+ * servers).
111
+ * - Use a proxy between your application and Anthropic's API for security,
112
+ * caching, or logging.
113
+ * - Employ a mock server for local development or testing.
114
+ *
115
+ * You may leave this property `undefined` to accept the default value.
116
+ */
61
117
  readonly apiUrl?: string | undefined
118
+
119
+ /**
120
+ * The Anthropic API version to use for requests.
121
+ *
122
+ * This version string determines which API schema and features are available
123
+ * for your requests. Different versions may have different capabilities,
124
+ * request/response formats, or available models.
125
+ *
126
+ * Defaults to `"2023-06-01"`.
127
+ *
128
+ * You should specify a version that:
129
+ * - Supports the features and models you need
130
+ * - Is stable and well-tested for your use case
131
+ * - Matches your application's integration requirements
132
+ *
133
+ * Consult Anthropic's API documentation for available versions and their
134
+ * differences.
135
+ */
62
136
  readonly anthropicVersion?: string | undefined
137
+
138
+ /**
139
+ * The organization ID to associate with API requests.
140
+ *
141
+ * This identifier links requests to a specific organization within your
142
+ * Anthropic account, enabling proper billing, usage tracking, and access
143
+ * control at the organizational level.
144
+ *
145
+ * Provide this when:
146
+ * - Your account belongs to multiple organizations
147
+ * - You need to ensure requests are billed to the correct organization
148
+ * - Organization-level access policies apply to your use case
149
+ *
150
+ * Leave `undefined` if you're using a personal account or the default
151
+ * organization.
152
+ */
63
153
  readonly organizationId?: Redacted.Redacted | undefined
154
+
155
+ /**
156
+ * The project ID to associate with API requests.
157
+ *
158
+ * This identifier scopes requests to a specific project within your
159
+ * organization, enabling granular resource management, billing allocation,
160
+ * and access control at the project level.
161
+ *
162
+ * Specify this when:
163
+ * - You have multiple projects and need to separate their API usage
164
+ * - Project-level billing or quota management is required
165
+ * - Access policies are configured at the project level
166
+ *
167
+ * Leave `undefined` to use the default project or when project-level
168
+ * scoping is not needed.
169
+ */
64
170
  readonly projectId?: Redacted.Redacted | undefined
171
+
172
+ /**
173
+ * A function to transform the underlying HTTP client before it's used for API requests.
174
+ *
175
+ * This transformation function receives the configured HTTP client and returns
176
+ * a modified version. It's applied after all standard client configuration
177
+ * (authentication, base URL, headers) but before any requests are made.
178
+ *
179
+ * Use this for:
180
+ * - Adding custom middleware (logging, metrics, caching)
181
+ * - Modifying request/response processing behavior
182
+ * - Adding custom retry logic or error handling
183
+ * - Integrating with monitoring or debugging tools
184
+ * - Applying organization-specific HTTP client policies
185
+ *
186
+ * The transformation is applied once during client initialization and affects
187
+ * all subsequent API requests made through this client instance.
188
+ *
189
+ * Leave `undefined` if no custom HTTP client behavior is needed.
190
+ */
65
191
  readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
66
- }): Effect.Effect<AnthropicClient.Service, never, HttpClient.HttpClient> =>
67
- Effect.gen(function*() {
68
- const httpClient = (yield* HttpClient.HttpClient).pipe(
69
- HttpClient.mapRequest((request) =>
70
- request.pipe(
71
- HttpClientRequest.prependUrl(
72
- options.apiUrl ?? "https://api.anthropic.com"
73
- ),
74
- options.apiKey
75
- ? HttpClientRequest.setHeader(
76
- "x-api-key",
77
- Redacted.value(options.apiKey)
78
- )
79
- : identity,
80
- HttpClientRequest.setHeader(
81
- "anthropic-version",
82
- options.anthropicVersion ?? "2023-06-01"
83
- ),
84
- HttpClientRequest.acceptJson
85
- )
86
- ),
87
- options.transformClient ? options.transformClient : identity
88
- )
89
- const httpClientOk = HttpClient.filterStatusOk(httpClient)
90
- const client = Generated.make(httpClient, {
91
- transformClient: (client) =>
92
- AnthropicConfig.getOrUndefined.pipe(
93
- Effect.map((config) => config?.transformClient ? config.transformClient(client) : client)
94
- )
95
- })
96
- const streamRequest = <A = unknown>(
97
- request: HttpClientRequest.HttpClientRequest
98
- ) =>
99
- httpClientOk.execute(request).pipe(
100
- Effect.map((r) => r.stream),
101
- Stream.unwrapScoped,
102
- Stream.decodeText(),
103
- Stream.pipeThroughChannel(Sse.makeChannel()),
104
- Stream.takeUntil((event) => event.event === "message_stop"),
105
- Stream.map((event) => JSON.parse(event.data) as A)
192
+ }) => Effect.Effect<Service, never, HttpClient.HttpClient | Scope.Scope> = Effect.fnUntraced(function*(options) {
193
+ const apiKeyHeader = "x-api-key"
194
+
195
+ yield* Effect.locallyScopedWith(Headers.currentRedactedNames, Arr.append(apiKeyHeader))
196
+
197
+ const httpClient = (yield* HttpClient.HttpClient).pipe(
198
+ HttpClient.mapRequest((request) =>
199
+ request.pipe(
200
+ HttpClientRequest.prependUrl(
201
+ options.apiUrl ?? "https://api.anthropic.com"
202
+ ),
203
+ options.apiKey
204
+ ? HttpClientRequest.setHeader(
205
+ apiKeyHeader,
206
+ Redacted.value(options.apiKey)
207
+ )
208
+ : identity,
209
+ HttpClientRequest.setHeader(
210
+ "anthropic-version",
211
+ options.anthropicVersion ?? "2023-06-01"
212
+ ),
213
+ HttpClientRequest.acceptJson
106
214
  )
107
- const stream = (request: StreamCompletionRequest) =>
108
- Stream.suspend(() => {
109
- const toolCalls = {} as Record<number, RawToolCall>
110
- let finishReason: AiResponse.FinishReason = "unknown"
111
- let reasoning:
112
- | {
113
- readonly content: Array<string>
114
- readonly signature?: string
115
- }
116
- | undefined = undefined
117
- let usage: AiResponse.Usage = {
118
- inputTokens: 0,
119
- outputTokens: 0,
120
- totalTokens: 0,
121
- reasoningTokens: 0,
122
- cacheReadInputTokens: 0,
123
- cacheWriteInputTokens: 0
124
- }
125
- const metadata: Record<string, unknown> = {}
126
- return streamRequest<MessageStreamEvent>(
127
- HttpClientRequest.post("/v1/messages", {
128
- body: HttpBody.unsafeJson({ ...request, stream: true })
129
- })
130
- ).pipe(
131
- Stream.filterMapEffect((chunk) => {
132
- const parts: Array<AiResponse.Part> = []
133
- switch (chunk.type) {
134
- case "message_start": {
135
- usage = {
136
- inputTokens: chunk.message.usage.input_tokens,
137
- outputTokens: chunk.message.usage.output_tokens,
138
- totalTokens: chunk.message.usage.input_tokens +
139
- chunk.message.usage.output_tokens,
140
- reasoningTokens: 0,
141
- cacheWriteInputTokens: chunk.message.usage.cache_creation_input_tokens ?? 0,
142
- cacheReadInputTokens: chunk.message.usage.cache_read_input_tokens ?? 0
143
- }
144
- parts.push(
145
- new AiResponse.MetadataPart(
146
- {
147
- id: chunk.message.id,
148
- model: chunk.message.model
149
- },
150
- constDisableValidation
151
- )
152
- )
153
- break
154
- }
155
- case "message_delta": {
156
- usage = {
157
- ...usage,
158
- outputTokens: chunk.usage.output_tokens
159
- }
160
- if (Predicate.isNotNullable(chunk.delta.stop_sequence)) {
161
- metadata.stopSequence = chunk.delta.stop_sequence
162
- }
163
- finishReason = InternalUtilities.resolveFinishReason(chunk.delta.stop_reason)
164
- break
165
- }
166
- case "message_stop": {
167
- parts.push(
168
- new AiResponse.FinishPart({
169
- usage,
170
- reason: finishReason,
171
- providerMetadata: { [InternalUtilities.ProviderMetadataKey]: metadata }
172
- }, constDisableValidation)
173
- )
174
- break
175
- }
176
- case "content_block_start": {
177
- const content = chunk.content_block
178
- switch (content.type) {
179
- case "text": {
180
- break
181
- }
182
- case "thinking": {
183
- reasoning = { content: [content.thinking] }
184
- break
185
- }
186
- case "tool_use": {
187
- toolCalls[chunk.index] = {
188
- id: content.id,
189
- name: content.name,
190
- params: ""
191
- }
192
- break
193
- }
194
- case "redacted_thinking": {
195
- parts.push(
196
- new AiResponse.RedactedReasoningPart(
197
- { redactedText: content.data },
198
- constDisableValidation
199
- )
200
- )
201
- break
202
- }
203
- }
204
- break
205
- }
206
- case "content_block_delta": {
207
- switch (chunk.delta.type) {
208
- case "text_delta": {
209
- parts.push(
210
- new AiResponse.TextPart(
211
- { text: chunk.delta.text },
212
- constDisableValidation
213
- )
214
- )
215
- break
216
- }
217
- case "thinking_delta": {
218
- if (Predicate.isNotUndefined(reasoning)) {
219
- reasoning.content.push(chunk.delta.thinking)
220
- }
221
- break
222
- }
223
- case "signature_delta": {
224
- if (Predicate.isNotUndefined(reasoning)) {
225
- reasoning = {
226
- ...reasoning,
227
- signature: chunk.delta.signature
228
- }
229
- }
230
- break
231
- }
232
- case "input_json_delta": {
233
- const tool = toolCalls[chunk.index]
234
- if (Predicate.isNotUndefined(tool)) {
235
- tool.params += chunk.delta.partial_json
236
- }
237
- break
238
- }
239
- // TODO: add support for citations (?)
240
- case "citations_delta": {
241
- break
242
- }
243
- }
244
- break
245
- }
246
- case "content_block_stop": {
247
- if (Predicate.isNotUndefined(toolCalls[chunk.index])) {
248
- const tool = toolCalls[chunk.index]
249
- try {
250
- // If the tool call has no parameters, the model sends an empty string.
251
- const inputJson = tool.params === "" ? "{}" : tool.params
252
- const params = JSON.parse(inputJson)
253
- parts.push(
254
- new AiResponse.ToolCallPart({
255
- id: AiInput.ToolCallId.make(tool.id, constDisableValidation),
256
- name: tool.name,
257
- params
258
- }, constDisableValidation)
259
- )
260
- delete toolCalls[chunk.index]
261
- // eslint-disable-next-line no-empty
262
- } catch {}
263
- }
264
- if (Predicate.isNotUndefined(reasoning)) {
265
- parts.push(
266
- new AiResponse.ReasoningPart({
267
- reasoningText: reasoning.content.join(""),
268
- signature: reasoning.signature
269
- }, constDisableValidation)
270
- )
271
- reasoning = undefined
272
- }
273
- break
274
- }
275
- case "error": {
276
- return Option.some(
277
- Effect.die(
278
- new AiError.AiError({
279
- module: "AnthropicClient",
280
- method: "stream",
281
- description: `${chunk.error.type}: ${chunk.error.message}`
282
- })
283
- )
284
- )
285
- }
286
- }
287
- return parts.length === 0
288
- ? Option.none()
289
- : Option.some(
290
- Effect.succeed(
291
- AiResponse.AiResponse.make(
292
- { parts },
293
- constDisableValidation
294
- )
295
- )
296
- )
215
+ ),
216
+ options.transformClient ? options.transformClient : identity
217
+ )
218
+
219
+ const client = Generated.make(httpClient, {
220
+ transformClient: (client) =>
221
+ AnthropicConfig.getOrUndefined.pipe(
222
+ Effect.map((config) => config?.transformClient ? config.transformClient(client) : client)
223
+ )
224
+ })
225
+
226
+ const streamRequest = <A, I, R>(
227
+ request: HttpClientRequest.HttpClientRequest,
228
+ schema: Schema.Schema<A, I, R>
229
+ ): Stream.Stream<A, AiError.AiError, R> => {
230
+ const decodeEvents = Schema.decode(Schema.ChunkFromSelf(Schema.parseJson(schema)))
231
+ return httpClient.execute(request).pipe(
232
+ Effect.map((r) => r.stream),
233
+ Stream.unwrapScoped,
234
+ Stream.decodeText(),
235
+ Stream.pipeThroughChannel(Sse.makeChannel()),
236
+ Stream.mapChunksEffect((chunk) => decodeEvents(Chunk.map(chunk, (event) => event.data))),
237
+ Stream.catchTags({
238
+ RequestError: (error) =>
239
+ AiError.HttpRequestError.fromRequestError({
240
+ module: "AnthropicClient",
241
+ method: "streamRequest",
242
+ error
243
+ }),
244
+ ResponseError: (error) =>
245
+ AiError.HttpResponseError.fromResponseError({
246
+ module: "AnthropicClient",
247
+ method: "streamRequest",
248
+ error
249
+ }),
250
+ ParseError: (error) =>
251
+ AiError.MalformedOutput.fromParseError({
252
+ module: "AnthropicClient",
253
+ method: "streamRequest",
254
+ error
297
255
  })
298
- )
299
256
  })
300
- return AnthropicClient.of({ client, streamRequest, stream })
257
+ )
258
+ }
259
+
260
+ const createMessage: (options: {
261
+ readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined
262
+ readonly payload: typeof Generated.BetaCreateMessageParams.Encoded
263
+ }) => Effect.Effect<Generated.BetaMessage, AiError.AiError> = Effect.fnUntraced(
264
+ function*(options) {
265
+ return yield* client.betaMessagesPost(options).pipe(
266
+ Effect.catchTags({
267
+ RequestError: (error) =>
268
+ AiError.HttpRequestError.fromRequestError({
269
+ module: "AnthropicClient",
270
+ method: "createMessage",
271
+ error
272
+ }),
273
+ ResponseError: (error) =>
274
+ AiError.HttpResponseError.fromResponseError({
275
+ module: "AnthropicClient",
276
+ method: "createMessage",
277
+ error
278
+ }),
279
+ BetaErrorResponse: (error) => {
280
+ return new AiError.HttpResponseError({
281
+ module: "AnthropicClient",
282
+ method: "createMessage",
283
+ cause: error.cause,
284
+ reason: "StatusCode",
285
+ request: {
286
+ hash: error.request.hash,
287
+ headers: error.request.headers,
288
+ method: error.request.method,
289
+ url: error.request.url,
290
+ urlParams: error.request.urlParams
291
+ },
292
+ response: {
293
+ headers: error.response.headers,
294
+ status: error.response.status
295
+ }
296
+ })
297
+ },
298
+ ParseError: (error) =>
299
+ AiError.MalformedOutput.fromParseError({
300
+ module: "AnthropicClient",
301
+ method: "createMessage",
302
+ error
303
+ })
304
+ })
305
+ )
306
+ }
307
+ )
308
+
309
+ const createMessageStream = (options: {
310
+ readonly params?: typeof Generated.BetaMessagesPostParams.Encoded | undefined
311
+ readonly payload: Omit<typeof Generated.BetaCreateMessageParams.Encoded, "stream">
312
+ }): Stream.Stream<MessageStreamEvent, AiError.AiError> => {
313
+ const request = HttpClientRequest.post("/v1/messages", {
314
+ headers: Headers.fromInput({
315
+ "anthropic-beta": options.params?.["anthropic-beta"] ?? undefined
316
+ }),
317
+ body: HttpBody.unsafeJson({ ...options.payload, stream: true })
318
+ })
319
+ return streamRequest(request, MessageStreamEvent).pipe(
320
+ Stream.takeUntil((event) => event.type === "message_stop")
321
+ )
322
+ }
323
+
324
+ return AnthropicClient.of({
325
+ client,
326
+ streamRequest,
327
+ createMessage,
328
+ createMessageStream
329
+ })
330
+ })
331
+
332
+ // =============================================================================
333
+ // Message Stream Schema
334
+ // =============================================================================
335
+
336
+ /**
337
+ * @since 1.0.0
338
+ * @category Schemas
339
+ */
340
+ export class PingEvent extends Schema.Class<PingEvent>(
341
+ "@effect/ai-anthropic/PingEvent"
342
+ )({
343
+ type: Schema.Literal("ping")
344
+ }) {}
345
+
346
+ /**
347
+ * @since 1.0.0
348
+ * @category Schemas
349
+ */
350
+ export class ErrorEvent extends Schema.Class<ErrorEvent>(
351
+ "@effect/ai-anthropic/ErrorEvent"
352
+ )({
353
+ type: Schema.Literal("error"),
354
+ error: Schema.Struct({
355
+ type: Schema.Literal(
356
+ "invalid_request_error",
357
+ "authentication_error",
358
+ "permission_error",
359
+ "not_found_error",
360
+ "request_too_large",
361
+ "rate_limit_error",
362
+ "api_error",
363
+ "overloaded_error"
364
+ ),
365
+ message: Schema.String
301
366
  })
367
+ }) {}
368
+
369
+ /**
370
+ * @since 1.0.0
371
+ * @category Schemas
372
+ */
373
+ export class MessageStartEvent extends Schema.Class<MessageStartEvent>(
374
+ "@effect/ai-anthropic/MessageStartEvent"
375
+ )({
376
+ type: Schema.Literal("message_start"),
377
+ message: Generated.BetaMessage
378
+ }) {}
379
+
380
+ /**
381
+ * @since 1.0.0
382
+ * @category Schemas
383
+ */
384
+ export class ServerToolUsage extends Schema.Class<ServerToolUsage>(
385
+ "@effect/ai-anthropic/ServerToolUsage"
386
+ )({
387
+ /**
388
+ * The number of web search tool requests.
389
+ */
390
+ web_search_requests: Schema.optionalWith(
391
+ Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))),
392
+ { default: () => 0 }
393
+ )
394
+ }) {}
395
+
396
+ /**
397
+ * @since 1.0.0
398
+ * @category Schemas
399
+ */
400
+ export class MessageDelta extends Schema.Class<MessageDelta>(
401
+ "@effect/ai-anthropic/MessageDelta"
402
+ )({
403
+ stop_reason: Schema.optionalWith(
404
+ Schema.NullOr(
405
+ Schema.Literal(
406
+ "end_turn",
407
+ "max_tokens",
408
+ "stop_sequence",
409
+ "tool_use",
410
+ "pause_turn",
411
+ "refusal"
412
+ )
413
+ ),
414
+ { default: () => null }
415
+ ),
416
+ stop_sequence: Schema.optionalWith(
417
+ Schema.NullOr(Schema.String),
418
+ { default: () => null }
419
+ )
420
+ }) {}
421
+
422
+ /**
423
+ * @since 1.0.0
424
+ * @category Schemas
425
+ */
426
+ export class MessageDeltaUsage extends Schema.Class<MessageDeltaUsage>(
427
+ "@effect/ai-anthropic/MessageDeltaUsage"
428
+ )({
429
+ /**
430
+ * The cumulative number of input tokens which were used.
431
+ */
432
+ input_tokens: Schema.optionalWith(
433
+ Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))),
434
+ { default: () => null }
435
+ ),
436
+ /**
437
+ * The cumulative number of output tokens which were used.
438
+ */
439
+ output_tokens: Schema.optionalWith(
440
+ Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))),
441
+ { default: () => null }
442
+ ),
443
+ /**
444
+ * The cumulative number of input tokens used to create the cache entry.
445
+ */
446
+ cache_creation_input_tokens: Schema.optionalWith(
447
+ Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))),
448
+ { default: () => null }
449
+ ),
450
+ /**
451
+ * The cumulative number of input tokens read from the cache.
452
+ */
453
+ cache_read_input_tokens: Schema.optionalWith(
454
+ Schema.NullOr(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))),
455
+ { default: () => null }
456
+ ),
457
+ /**
458
+ * The number of server tool requests.
459
+ */
460
+ server_tool_use: Schema.optionalWith(
461
+ Schema.NullOr(ServerToolUsage),
462
+ { default: () => null }
463
+ )
464
+ }) {}
465
+
466
+ /**
467
+ * @since 1.0.0
468
+ * @category Schemas
469
+ */
470
+ export class MessageDeltaEvent extends Schema.Class<MessageDeltaEvent>(
471
+ "@effect/ai-anthropic/MessageDeltaEvent"
472
+ )({
473
+ type: Schema.Literal("message_delta"),
474
+ delta: MessageDelta,
475
+ /**
476
+ * Billing and rate-limit usage.
477
+ *
478
+ * Anthropic's API bills and rate-limits by token counts, as tokens represent
479
+ * the underlying cost to our systems.
480
+ *
481
+ * Under the hood, the API transforms requests into a format suitable for the
482
+ * model. The model's output then goes through a parsing stage before becoming
483
+ * an API response. As a result, the token counts in `usage` will not match
484
+ * one-to-one with the exact visible content of an API request or response.
485
+ *
486
+ * For example, `output_tokens` will be non-zero, even for an empty string
487
+ * response from Claude.\n\nTotal input tokens in a request is the summation
488
+ * of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`.
489
+ */
490
+ usage: MessageDeltaUsage
491
+ }) {}
492
+
493
+ /**
494
+ * @since 1.0.0
495
+ * @category Schemas
496
+ */
497
+ export class MessageStopEvent extends Schema.Class<MessageStopEvent>(
498
+ "@effect/ai-anthropic/MessageStopEvent"
499
+ )({
500
+ type: Schema.Literal("message_stop")
501
+ }) {}
502
+
503
+ /**
504
+ * @since 1.0.0
505
+ * @category Schemas
506
+ */
507
+ export class ContentBlockStartEvent extends Schema.Class<ContentBlockStartEvent>(
508
+ "@effect/ai-anthropic/ContentBlockStartEvent"
509
+ )({
510
+ type: Schema.Literal("content_block_start"),
511
+ index: Schema.Int,
512
+ content_block: Generated.BetaContentBlock
513
+ }) {}
514
+
515
+ /**
516
+ * @since 1.0.0
517
+ * @category Schemas
518
+ */
519
+ export class CitationsDelta extends Schema.Class<CitationsDelta>(
520
+ "@effect/ai-anthropic/CitationsDelta"
521
+ )({
522
+ type: Schema.Literal("citations_delta"),
523
+ citation: Schema.Union(
524
+ Generated.BetaResponseCharLocationCitation,
525
+ Generated.BetaResponsePageLocationCitation,
526
+ Generated.BetaResponseContentBlockLocationCitation,
527
+ Generated.BetaResponseWebSearchResultLocationCitation,
528
+ Generated.BetaResponseSearchResultLocationCitation
529
+ )
530
+ }) {}
531
+
532
+ /**
533
+ * @since 1.0.0
534
+ * @category Schemas
535
+ */
536
+ export class InputJsonContentBlockDelta extends Schema.Class<InputJsonContentBlockDelta>(
537
+ "@effect/ai-anthropic/InputJsonContentBlockDelta"
538
+ )({
539
+ type: Schema.Literal("input_json_delta"),
540
+ partial_json: Schema.String
541
+ }) {}
542
+
543
+ /**
544
+ * @since 1.0.0
545
+ * @category Schemas
546
+ */
547
+ export class SignatureContentBlockDelta extends Schema.Class<SignatureContentBlockDelta>(
548
+ "@effect/ai-anthropic/SignatureContentBlockDelta"
549
+ )({
550
+ type: Schema.Literal("signature_delta"),
551
+ signature: Schema.String
552
+ }) {}
553
+
554
+ /**
555
+ * @since 1.0.0
556
+ * @category Schemas
557
+ */
558
+ export class TextContentBlockDelta extends Schema.Class<TextContentBlockDelta>(
559
+ "@effect/ai-anthropic/TextContentBlockDelta"
560
+ )({
561
+ type: Schema.Literal("text_delta"),
562
+ text: Schema.String
563
+ }) {}
564
+
565
+ /**
566
+ * @since 1.0.0
567
+ * @category Schemas
568
+ */
569
+ export class ThinkingContentBlockDelta extends Schema.Class<ThinkingContentBlockDelta>(
570
+ "@effect/ai-anthropic/ThinkingContentBlockDelta"
571
+ )({
572
+ type: Schema.Literal("thinking_delta"),
573
+ thinking: Schema.String
574
+ }) {}
575
+
576
+ /**
577
+ * @since 1.0.0
578
+ * @category Schemas
579
+ */
580
+ export class ContentBlockDeltaEvent extends Schema.Class<ContentBlockDeltaEvent>(
581
+ "@effect/ai-anthropic/ContentBlockDeltaEvent"
582
+ )({
583
+ type: Schema.Literal("content_block_delta"),
584
+ index: Schema.Int,
585
+ delta: Schema.Union(
586
+ CitationsDelta,
587
+ InputJsonContentBlockDelta,
588
+ SignatureContentBlockDelta,
589
+ TextContentBlockDelta,
590
+ ThinkingContentBlockDelta
591
+ )
592
+ }) {}
593
+
594
+ /**
595
+ * @since 1.0.0
596
+ * @category Schemas
597
+ */
598
+ export class ContentBlockStopEvent extends Schema.Class<ContentBlockStopEvent>(
599
+ "@effect/ai-anthropic/ContentBlockStopEvent"
600
+ )({
601
+ type: Schema.Literal("content_block_stop"),
602
+ index: Schema.Int
603
+ }) {}
604
+
605
+ /**
606
+ * @since 1.0.0
607
+ * @category Schemas
608
+ */
609
+ export const MessageStreamEvent = Schema.Union(
610
+ PingEvent,
611
+ ErrorEvent,
612
+ MessageStartEvent,
613
+ MessageDeltaEvent,
614
+ MessageStopEvent,
615
+ ContentBlockStartEvent,
616
+ ContentBlockDeltaEvent,
617
+ ContentBlockStopEvent
618
+ )
619
+
620
+ /**
621
+ * @since 1.0.0
622
+ * @category Models
623
+ */
624
+ export type MessageStreamEvent = typeof MessageStreamEvent.Type
302
625
 
303
626
  /**
304
627
  * @since 1.0.0
305
628
  * @category Layers
306
629
  */
307
630
  export const layer = (options: {
631
+ /**
632
+ * The API key that will be used to authenticate with Anthropic's API.
633
+ *
634
+ * The key is wrapped in a `Redacted` type to prevent accidental logging or
635
+ * exposure in debugging output, helping maintain security best practices.
636
+ *
637
+ * The key is automatically included in the `x-api-key` header for all API
638
+ * requests made through this client, which is automatically redacted in logs
639
+ * output by Effect loggers.
640
+ *
641
+ * Leave `undefined` if authentication will be handled through other means
642
+ * (e.g., environment-based authentication, proxy authentication, or when
643
+ * using a mock server that doesn't require authentication).
644
+ */
308
645
  readonly apiKey?: Redacted.Redacted | undefined
646
+ /**
647
+ * The base URL endpoint used to communicate with Anthropic's API.
648
+ *
649
+ * This property determines the HTTP destination for all API requests made by
650
+ * this client.
651
+ *
652
+ * Defaults to `"https://api.anthropic.com"`.
653
+ *
654
+ * Override this value when you need to:
655
+ * - Point to a different Anthropic environment (e.g., staging or sandbox
656
+ * servers).
657
+ * - Use a proxy between your application and Anthropic's API for security,
658
+ * caching, or logging.
659
+ * - Employ a mock server for local development or testing.
660
+ *
661
+ * You may leave this property `undefined` to accept the default value.
662
+ */
309
663
  readonly apiUrl?: string | undefined
664
+ /**
665
+ * The Anthropic API version to use for requests.
666
+ *
667
+ * This version string determines which API schema and features are available
668
+ * for your requests. Different versions may have different capabilities,
669
+ * request/response formats, or available models.
670
+ *
671
+ * Defaults to `"2023-06-01"`.
672
+ *
673
+ * You should specify a version that:
674
+ * - Supports the features and models you need
675
+ * - Is stable and well-tested for your use case
676
+ * - Matches your application's integration requirements
677
+ *
678
+ * Consult Anthropic's API documentation for available versions and their
679
+ * differences.
680
+ */
310
681
  readonly anthropicVersion?: string | undefined
682
+ /**
683
+ * A function to transform the underlying HTTP client before it's used for API requests.
684
+ *
685
+ * This transformation function receives the configured HTTP client and returns
686
+ * a modified version. It's applied after all standard client configuration
687
+ * (authentication, base URL, headers) but before any requests are made.
688
+ *
689
+ * Use this for:
690
+ * - Adding custom middleware (logging, metrics, caching)
691
+ * - Modifying request/response processing behavior
692
+ * - Adding custom retry logic or error handling
693
+ * - Integrating with monitoring or debugging tools
694
+ * - Applying organization-specific HTTP client policies
695
+ *
696
+ * The transformation is applied once during client initialization and affects
697
+ * all subsequent API requests made through this client instance.
698
+ *
699
+ * Leave `undefined` if no custom HTTP client behavior is needed.
700
+ */
311
701
  readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
312
- }): Layer.Layer<AnthropicClient, never, HttpClient.HttpClient> => Layer.effect(AnthropicClient, make(options))
702
+ }): Layer.Layer<AnthropicClient, never, HttpClient.HttpClient> => Layer.scoped(AnthropicClient, make(options))
313
703
 
314
704
  /**
315
705
  * @since 1.0.0
@@ -317,128 +707,82 @@ export const layer = (options: {
317
707
  */
318
708
  export const layerConfig = (
319
709
  options: {
710
+ /**
711
+ * The API key that will be used to authenticate with Anthropic's API.
712
+ *
713
+ * The key is wrapped in a `Redacted` type to prevent accidental logging or
714
+ * exposure in debugging output, helping maintain security best practices.
715
+ *
716
+ * The key is automatically included in the `x-api-key` header for all API
717
+ * requests made through this client, which is automatically redacted in logs
718
+ * output by Effect loggers.
719
+ *
720
+ * Leave `undefined` if authentication will be handled through other means
721
+ * (e.g., environment-based authentication, proxy authentication, or when
722
+ * using a mock server that doesn't require authentication).
723
+ */
320
724
  readonly apiKey?: Config.Config<Redacted.Redacted | undefined> | undefined
725
+ /**
726
+ * The base URL endpoint used to communicate with Anthropic's API.
727
+ *
728
+ * This property determines the HTTP destination for all API requests made by
729
+ * this client.
730
+ *
731
+ * Defaults to `"https://api.anthropic.com"`.
732
+ *
733
+ * Override this value when you need to:
734
+ * - Point to a different Anthropic environment (e.g., staging or sandbox
735
+ * servers).
736
+ * - Use a proxy between your application and Anthropic's API for security,
737
+ * caching, or logging.
738
+ * - Employ a mock server for local development or testing.
739
+ *
740
+ * You may leave this property `undefined` to accept the default value.
741
+ */
321
742
  readonly apiUrl?: Config.Config<string | undefined> | undefined
743
+ /**
744
+ * The Anthropic API version to use for requests.
745
+ *
746
+ * This version string determines which API schema and features are available
747
+ * for your requests. Different versions may have different capabilities,
748
+ * request/response formats, or available models.
749
+ *
750
+ * Defaults to `"2023-06-01"`.
751
+ *
752
+ * You should specify a version that:
753
+ * - Supports the features and models you need
754
+ * - Is stable and well-tested for your use case
755
+ * - Matches your application's integration requirements
756
+ *
757
+ * Consult Anthropic's API documentation for available versions and their
758
+ * differences.
759
+ */
322
760
  readonly anthropicVersion?: Config.Config<string | undefined> | undefined
761
+ /**
762
+ * A function to transform the underlying HTTP client before it's used for API requests.
763
+ *
764
+ * This transformation function receives the configured HTTP client and returns
765
+ * a modified version. It's applied after all standard client configuration
766
+ * (authentication, base URL, headers) but before any requests are made.
767
+ *
768
+ * Use this for:
769
+ * - Adding custom middleware (logging, metrics, caching)
770
+ * - Modifying request/response processing behavior
771
+ * - Adding custom retry logic or error handling
772
+ * - Integrating with monitoring or debugging tools
773
+ * - Applying organization-specific HTTP client policies
774
+ *
775
+ * The transformation is applied once during client initialization and affects
776
+ * all subsequent API requests made through this client instance.
777
+ *
778
+ * Leave `undefined` if no custom HTTP client behavior is needed.
779
+ */
323
780
  readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined
324
781
  }
325
782
  ): Layer.Layer<AnthropicClient, ConfigError, HttpClient.HttpClient> => {
326
783
  const { transformClient, ...configs } = options
327
784
  return Config.all(configs).pipe(
328
785
  Effect.flatMap((configs) => make({ ...configs, transformClient })),
329
- Layer.effect(AnthropicClient)
786
+ Layer.scoped(AnthropicClient)
330
787
  )
331
788
  }
332
-
333
- /**
334
- * @since 1.0.0
335
- * @category Models
336
- */
337
- export type StreamCompletionRequest = Omit<
338
- typeof Generated.CreateMessageParams.Encoded,
339
- "stream"
340
- >
341
-
342
- type MessageStreamEvent =
343
- | ErrorEvent
344
- | MessageStartEvent
345
- | MessageDeltaEvent
346
- | MessageStopEvent
347
- | ContentBlockStartEvent
348
- | ContentBlockDeltaEvent
349
- | ContentBlockStopEvent
350
-
351
- interface MessageStartEvent {
352
- readonly type: "message_start"
353
- readonly message: typeof Generated.Message.Encoded
354
- }
355
-
356
- interface MessageDeltaEvent {
357
- readonly type: "message_delta"
358
- readonly delta: {
359
- readonly stop_reason:
360
- | "end_turn"
361
- | "max_tokens"
362
- | "stop_sequence"
363
- | "tool_use"
364
- readonly stop_sequence: string | null
365
- }
366
- readonly usage: {
367
- readonly output_tokens: number
368
- }
369
- }
370
-
371
- interface MessageStopEvent {
372
- readonly type: "message_stop"
373
- }
374
-
375
- interface ContentBlockStartEvent {
376
- readonly type: "content_block_start"
377
- readonly index: number
378
- readonly content_block: typeof Generated.ContentBlock.Encoded
379
- }
380
-
381
- interface ContentBlockDeltaEvent {
382
- readonly type: "content_block_delta"
383
- readonly index: number
384
- readonly delta:
385
- | CitationsDelta
386
- | InputJsonContentBlockDelta
387
- | SignatureDelta
388
- | TextContentBlockDelta
389
- | ThinkingDelta
390
- }
391
-
392
- interface CitationsDelta {
393
- readonly type: "citations_delta"
394
- readonly citation: NonNullable<
395
- (typeof Generated.ResponseTextBlock.Encoded)["citations"]
396
- >[number]
397
- }
398
-
399
- interface InputJsonContentBlockDelta {
400
- readonly type: "input_json_delta"
401
- readonly partial_json: string
402
- }
403
-
404
- interface SignatureDelta {
405
- readonly type: "signature_delta"
406
- readonly signature: string
407
- }
408
-
409
- interface TextContentBlockDelta {
410
- readonly type: "text_delta"
411
- readonly text: string
412
- }
413
-
414
- interface ThinkingDelta {
415
- readonly type: "thinking_delta"
416
- readonly thinking: string
417
- }
418
-
419
- interface ContentBlockStopEvent {
420
- readonly type: "content_block_stop"
421
- readonly index: number
422
- }
423
-
424
- interface ErrorEvent {
425
- readonly type: "error"
426
- readonly error: {
427
- readonly type:
428
- | "api_error"
429
- | "authentication_error"
430
- | "invalid_request_error"
431
- | "not_found_error"
432
- | "overloaded_error"
433
- | "permission_error"
434
- | "rate_limit_error"
435
- | "request_too_large"
436
- readonly message: string
437
- }
438
- }
439
-
440
- type RawToolCall = {
441
- readonly id: string
442
- readonly name: string
443
- params: string
444
- }