1import * as AiError from "effect/ai/AiError"
2import type * as Response from "effect/ai/Response"
3import * as Duration from "effect/Duration"
4import * as Effect from "effect/Effect"
5import type * as Sse from "effect/encoding/Sse"
6import { dual } from "effect/Function"
7import type * as HttpClientError from "effect/http/HttpClientError"
8import type * as HttpClientRequest from "effect/http/HttpClientRequest"
9import type * as HttpClientResponse from "effect/http/HttpClientResponse"
10import * as Number from "effect/Number"
11import * as Option from "effect/Option"
12import * as Predicate from "effect/Predicate"
13import * as Redactable from "effect/Redactable"
14import * as Schema from "effect/Schema"
15import type { AnthropicErrorMetadata } from "../AnthropicError.ts"
16import type * as Generated from "../Generated.ts"
17
18// =============================================================================
19// Anthropic Error Body Schema
20// =============================================================================
21
22/** @internal */
23export const AnthropicErrorBody = Schema.Struct({
24 type: Schema.Literal("error"),
25 error: Schema.Struct({
26 type: Schema.String,
27 message: Schema.String
28 })
29})
30
31/** @internal */
32export type AnthropicClientErrorBody = {
33 readonly type: "error"
34 readonly error: {
35 readonly type: string
36 readonly message: string
37 }
38 readonly request_id: string | null
39}
40
41// =============================================================================
42// Error Mappers
43// =============================================================================
44
45/** @internal */
46export const mapSchemaError = dual<
47 (method: string) => (error: Schema.SchemaError) => AiError.AiError,
48 (error: Schema.SchemaError, method: string) => AiError.AiError
49>(2, (error, method) =>
50 AiError.make({
51 module: "AnthropicClient",
52 method,
53 reason: AiError.InvalidOutputError.fromSchemaError(error)
54 }))
55
56/** @internal */
57export const mapSseError = dual<
58 (method: string) => (error: Sse.SseError) => AiError.AiError,
59 (error: Sse.SseError, method: string) => AiError.AiError
60>(2, (error, method) =>
61 AiError.make({
62 module: "AnthropicClient",
63 method,
64 reason: new AiError.InvalidOutputError({ description: error.message })
65 }))
66
67/** @internal */
68export const mapClientError = dual<
69 (method: string) => (error: Generated.AnthropicClientError<string, AnthropicClientErrorBody>) => AiError.AiError,
70 (error: Generated.AnthropicClientError<string, AnthropicClientErrorBody>, method: string) => AiError.AiError
71>(2, (error, method) => {
72 const { request, response, cause } = error
73 const status = response.status
74 const headers = response.headers as Record<string, string>
75 const metadata: AnthropicErrorMetadata = {
76 errorType: cause.error.type,
77 requestId: cause.request_id
78 }
79 const http = buildHttpContext({ request, response, body: JSON.stringify(cause) })
80 const reason = mapStatusCodeToReason({
81 status,
82 headers,
83 message: cause.error.message,
84 metadata,
85 http
86 })
87 return AiError.make({ module: "AnthropicClient", method, reason })
88})
89
90/** @internal */
91export const mapHttpClientError = dual<
92 (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect<never, AiError.AiError>,
93 (error: HttpClientError.HttpClientError, method: string) => Effect.Effect<never, AiError.AiError>
94>(2, (error, method) => {
95 const reason = error.reason
96 switch (reason._tag) {
97 case "TransportError": {
98 return Effect.fail(AiError.make({
99 module: "AnthropicClient",
100 method,
101 reason: new AiError.NetworkError({
102 reason: "TransportError",
103 description: reason.description,
104 request: buildHttpRequestDetails(reason.request)
105 })
106 }))
107 }
108 case "EncodeError": {
109 return Effect.fail(AiError.make({
110 module: "AnthropicClient",
111 method,
112 reason: new AiError.NetworkError({
113 reason: "EncodeError",
114 description: reason.description,
115 request: buildHttpRequestDetails(reason.request)
116 })
117 }))
118 }
119 case "InvalidUrlError": {
120 return Effect.fail(AiError.make({
121 module: "AnthropicClient",
122 method,
123 reason: new AiError.NetworkError({
124 reason: "InvalidUrlError",
125 description: reason.description,
126 request: buildHttpRequestDetails(reason.request)
127 })
128 }))
129 }
130 case "StatusCodeError": {
131 return mapStatusCodeError(reason, method)
132 }
133 case "DecodeError": {
134 return Effect.fail(AiError.make({
135 module: "AnthropicClient",
136 method,
137 reason: new AiError.InvalidOutputError({
138 description: reason.description ?? "Failed to decode response"
139 })
140 }))
141 }
142 case "EmptyBodyError": {
143 return Effect.fail(AiError.make({
144 module: "AnthropicClient",
145 method,
146 reason: new AiError.InvalidOutputError({
147 description: reason.description ?? "Response body was empty"
148 })
149 }))
150 }
151 }
152})
153
154/** @internal */
155const mapStatusCodeError = Effect.fnUntraced(function*(
156 error: HttpClientError.StatusCodeError,
157 method: string
158) {
159 const { request, response, description } = error
160 const status = response.status
161 const headers = response.headers as Record<string, string>
162 const requestId = headers["request-id"]
163
164 let body: string | undefined = description
165 if (!description || !description.startsWith("{")) {
166 const responseBody = yield* Effect.option(response.text)
167 if (Option.isSome(responseBody) && responseBody.value) {
168 body = responseBody.value
169 }
170 }
171
172 let json: unknown = undefined
173 // @effect-diagnostics effect/tryCatchInEffectGen:off
174 try {
175 json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined
176 } catch {
177 json = undefined
178 }
179 const decoded = Schema.decodeUnknownOption(AnthropicErrorBody)(json)
180
181 const reason = mapStatusCodeToReason({
182 status,
183 headers,
184 message: Option.isSome(decoded) ? decoded.value.error.message : undefined,
185 http: buildHttpContext({ request, response, body }),
186 metadata: {
187 errorType: Option.isSome(decoded) ? decoded.value.error.type : null,
188 requestId: requestId ?? null
189 }
190 })
191
192 return yield* AiError.make({ module: "AnthropicClient", method, reason })
193})
194
195// =============================================================================
196// Rate Limits
197// =============================================================================
198
199/** @internal */
200export const parseRateLimitHeaders = (headers: Record<string, string>) => {
201 const retryAfterRaw = headers["retry-after"]
202 let retryAfter: Duration.Duration | undefined
203 if (Predicate.isNotUndefined(retryAfterRaw)) {
204 const parsed = Number.parse(retryAfterRaw)
205 if (Option.isSome(parsed)) {
206 retryAfter = Duration.seconds(parsed.value)
207 }
208 }
209 const requestsLimitRaw = headers["anthropic-ratelimit-requests-limit"]
210 const requestsRemainingRaw = headers["anthropic-ratelimit-requests-remaining"]
211 const tokensLimitRaw = headers["anthropic-ratelimit-tokens-limit"]
212 const tokensRemainingRaw = headers["anthropic-ratelimit-tokens-remaining"]
213 return {
214 retryAfter,
215 requestsLimit: Predicate.isNotUndefined(requestsLimitRaw) ? Option.getOrNull(Number.parse(requestsLimitRaw)) : null,
216 requestsRemaining: Predicate.isNotUndefined(requestsRemainingRaw)
217 ? Option.getOrNull(Number.parse(requestsRemainingRaw))
218 : null,
219 requestsReset: headers["anthropic-ratelimit-requests-reset"] ?? null,
220 tokensLimit: Predicate.isNotUndefined(tokensLimitRaw) ? Option.getOrNull(Number.parse(tokensLimitRaw)) : null,
221 tokensRemaining: Predicate.isNotUndefined(tokensRemainingRaw)
222 ? Option.getOrNull(Number.parse(tokensRemainingRaw))
223 : null,
224 tokensReset: headers["anthropic-ratelimit-tokens-reset"] ?? null
225 }
226}
227
228// =============================================================================
229// HTTP Context
230// =============================================================================
231
232/** @internal */
233export const buildHttpRequestDetails = (
234 request: HttpClientRequest.HttpClientRequest
235): typeof Response.HttpRequestDetails.Type => ({
236 method: request.method,
237 url: request.url,
238 urlParams: Array.from(request.urlParams),
239 hash: Option.getOrUndefined(request.hash),
240 headers: Redactable.redact(request.headers) as Record<string, string>
241})
242
243/** @internal */
244export const buildHttpContext = (params: {
245 readonly request: HttpClientRequest.HttpClientRequest
246 readonly response?: HttpClientResponse.HttpClientResponse
247 readonly body?: string | undefined
248}): typeof AiError.HttpContext.Type => ({
249 request: buildHttpRequestDetails(params.request),
250 response: Predicate.isNotUndefined(params.response)
251 ? {
252 status: params.response.status,
253 headers: Redactable.redact(params.response.headers) as Record<string, string>
254 }
255 : undefined,
256 body: params.body
257})
258
259// =============================================================================
260// HTTP Status Code
261// =============================================================================
262
263/** @internal */
264export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: {
265 readonly status: number
266 readonly headers: Record<string, string>
267 readonly message: string | undefined
268 readonly metadata: AnthropicErrorMetadata
269 readonly http: typeof AiError.HttpContext.Type
270}): AiError.AiErrorReason => {
271 const errorDescription = AiError.buildErrorDescription({
272 status,
273 message,
274 method: http.request.method,
275 url: http.request.url,
276 errorType: metadata.errorType,
277 requestId: metadata.requestId,
278 body: http.body
279 })
280
281 switch (status) {
282 case 400:
283 return new AiError.InvalidRequestError({
284 description: errorDescription,
285 metadata: { anthropic: metadata },
286 http
287 })
288 case 401:
289 return new AiError.AuthenticationError({
290 kind: "InvalidKey",
291 description: errorDescription,
292 metadata: { anthropic: metadata },
293 http
294 })
295 case 403:
296 return new AiError.AuthenticationError({
297 kind: "InsufficientPermissions",
298 description: errorDescription,
299 metadata: { anthropic: metadata },
300 http
301 })
302 case 404:
303 return new AiError.InvalidRequestError({
304 description: errorDescription,
305 metadata: { anthropic: metadata },
306 http
307 })
308 case 422:
309 return new AiError.InvalidRequestError({
310 description: errorDescription,
311 metadata: { anthropic: metadata },
312 http
313 })
314 case 429: {
315 const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers)
316 return new AiError.RateLimitError({
317 retryAfter,
318 metadata: {
319 anthropic: {
320 ...metadata,
321 ...rateLimitMetadata
322 }
323 },
324 http
325 })
326 }
327 case 529:
328 return new AiError.InternalProviderError({
329 description: message ?? "Anthropic API is overloaded",
330 metadata: { anthropic: metadata },
331 http
332 })
333 default:
334 if (status >= 500) {
335 return new AiError.InternalProviderError({
336 description: message ?? "Server error",
337 metadata: { anthropic: metadata },
338 http
339 })
340 }
341 return new AiError.UnknownError({
342 description: message,
343 metadata: { anthropic: metadata },
344 http
345 })
346 }
347}