@effect/ai-anthropic 0.16.2 → 0.17.1

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