1/**
2 * Defines shared errors for AI operations.
3 *
4 * `AiError` records where a failure happened and stores the detailed reason in a
5 * `reason` field. Those reasons cover transport problems, provider responses,
6 * rate limits, authentication, content policy failures, invalid requests,
7 * invalid output, unsupported schemas, tool failures, invalid user input, and
8 * unknown failures. This module also includes metadata schemas, guards,
9 * constructors, and helpers for converting HTTP response information into AI
10 * error reasons.
11 *
12 * @stability unstable
13 * @since 4.0.0
14 */
15import * as Duration from "../Duration.ts"
16import * as Effect from "../Effect.ts"
17import type * as HttpClientError from "../http/HttpClientError.ts"
18import * as InternalRecord from "../internal/record.ts"
19import * as Option from "../Option.ts"
20import * as Predicate from "../Predicate.ts"
21import { redact } from "../Redactable.ts"
22import * as Redacted from "../Redacted.ts"
23import * as Schema from "../Schema.ts"
24
25/**
26 * Schema for HTTP requests to an AI provider.
27 *
28 * **Example** (Describing an HTTP request)
29 *
30 * ```ts import.meta.vitest
31 * import type { AiError } from "effect/ai"
32 *
33 * const requestDetails: typeof AiError.HttpRequestDetails.Type = {
34 * method: "POST",
35 * url: "https://api.openai.com/v1/responses",
36 * urlParams: [],
37 * hash: undefined,
38 * headers: { "Content-Type": "application/json" }
39 * }
40 * const result = [requestDetails.method, requestDetails.urlParams] // => ["POST", []]
41 * ```
42 *
43 * @stability unstable
44 * @category schemas
45 * @since 4.0.0
46 */
47export const HttpRequestDetails = Schema.Struct({
48 method: Schema.Literals(["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE", "QUERY"]),
49 url: Schema.String,
50 urlParams: Schema.Array(Schema.Tuple([Schema.String, Schema.String])),
51 hash: Schema.optional(Schema.String),
52 headers: Schema.Record(
53 Schema.String,
54 Schema.Union([
55 Schema.String,
56 Schema.Redacted(Schema.String)
57 ])
58 )
59}).annotate({ identifier: "HttpRequestDetails" })
60
61/**
62 * Schema for HTTP responses from an AI provider.
63 *
64 * **Example** (Describing an HTTP response)
65 *
66 * ```ts import.meta.vitest
67 * import type { AiError } from "effect/ai"
68 *
69 * const responseDetails: typeof AiError.HttpResponseDetails.Type = {
70 * status: 200,
71 * headers: {
72 * "Content-Type": "application/json",
73 * "X-Request-Id": "req_abc123"
74 * }
75 * }
76 * const result = [responseDetails.status, responseDetails.headers["X-Request-Id"]] // => [200, "req_abc123"]
77 * ```
78 *
79 * @stability unstable
80 * @category schemas
81 * @since 4.0.0
82 */
83export const HttpResponseDetails = Schema.Struct({
84 status: Schema.Int,
85 headers: Schema.Record(
86 Schema.String,
87 Schema.Union([
88 Schema.String,
89 Schema.Redacted(Schema.String)
90 ])
91 )
92}).annotate({ identifier: "HttpResponseDetails" })
93
94const ReasonTypeId = "~effect/ai/AiError/Reason" as const
95
96const providerMetadataWithDefaults = <Metadata extends ProviderMetadata>() =>
97 (ProviderMetadata as unknown as typeof ProviderMetadata & Schema.Schema<Metadata>).pipe(
98 Schema.withConstructorDefault(Effect.succeed({})),
99 Schema.withDecodingDefault(Effect.succeed({}))
100 )
101
102const redactHeaders = (headers: Record<string, string>): Record<string, string> => {
103 const redacted = redact(headers) as Record<string, string | Redacted.Redacted>
104 const result: Record<string, string> = {}
105 for (const key in redacted) {
106 const value = redacted[key]
107 InternalRecord.assignProperty(result, key, Redacted.isRedacted(value) ? value.toString() : value)
108 }
109 return result
110}
111
112// =============================================================================
113// Http Request Error
114// =============================================================================
115
116/**
117 * Error indicating a network-level failure before receiving a response.
118 *
119 * **Details**
120 *
121 * This error is raised when issues arise before receiving an HTTP response,
122 * such as network connectivity problems, request encoding issues, or invalid
123 * URLs.
124 *
125 * **Example** (Creating a network error)
126 *
127 * ```ts import.meta.vitest
128 * import { AiError } from "effect/ai"
129 *
130 * const error = new AiError.NetworkError({
131 * reason: "TransportError",
132 * request: {
133 * method: "POST",
134 * url: "https://api.openai.com/v1/completions",
135 * urlParams: [],
136 * hash: undefined,
137 * headers: { "Content-Type": "application/json" }
138 * },
139 * description: "Connection timeout after 30 seconds"
140 * })
141 *
142 * const result = [error.reason, error.isRetryable] // => ["TransportError", true]
143 * ```
144 *
145 * @stability unstable
146 * @category errors
147 * @since 4.0.0
148 */
149export class NetworkError extends Schema.Error<NetworkError>(
150 "effect/ai/AiError/NetworkError"
151)({
152 _tag: Schema.tag("NetworkError"),
153 reason: Schema.Literals(["TransportError", "EncodeError", "InvalidUrlError"]),
154 request: HttpRequestDetails,
155 description: Schema.optional(Schema.String)
156}) {
157 /**
158 * Marks `NetworkError` as a semantic AI error reason for runtime guards.
159 *
160 * @stability unstable
161 * @since 4.0.0
162 */
163 readonly [ReasonTypeId] = ReasonTypeId
164
165 /**
166 * Transport errors are retryable; encoding and URL errors are not.
167 *
168 * @stability unstable
169 * @since 4.0.0
170 */
171 get isRetryable(): boolean {
172 return this.reason === "TransportError"
173 }
174
175 /**
176 * Creates a NetworkError from a platform HttpClientError.RequestError.
177 *
178 * **Example** (Creating a network error from a request error)
179 *
180 * ```ts import.meta.vitest
181 * import { AiError } from "effect/ai"
182 * import { HttpClientError, HttpClientRequest } from "effect/http"
183 *
184 * const platformError = new HttpClientError.TransportError({
185 * request: HttpClientRequest.get("https://example.com/models"),
186 * description: "Connection refused"
187 * })
188 *
189 * const aiError = AiError.NetworkError.fromRequestError(platformError)
190 * aiError.reason // => "TransportError"
191 * ```
192 *
193 * @stability unstable
194 * @since 4.0.0
195 */
196 static fromRequestError(error: HttpClientError.RequestError): NetworkError {
197 return new NetworkError({
198 description: error.description,
199 reason: error._tag,
200 request: {
201 hash: Option.getOrUndefined(error.request.hash),
202 headers: redactHeaders(error.request.headers),
203 method: error.request.method,
204 url: error.request.url,
205 urlParams: Array.from(error.request.urlParams)
206 }
207 })
208 }
209
210 override get message(): string {
211 const methodAndUrl = `${this.request.method} ${this.request.url}`
212
213 let baseMessage = this.description
214 ? `${this.reason}: ${this.description}`
215 : `${this.reason}: A network error occurred.`
216
217 baseMessage += ` (${methodAndUrl})`
218
219 let suggestion = ""
220 switch (this.reason) {
221 case "EncodeError": {
222 suggestion += "Check that the request body data is properly formatted and matches the expected content type."
223 break
224 }
225
226 case "InvalidUrlError": {
227 suggestion += "Verify that the URL format is correct and that all required parameters have been provided."
228 suggestion += " Check for any special characters that may need encoding."
229 break
230 }
231
232 case "TransportError": {
233 suggestion += "Check your network connection and verify that the requested URL is accessible."
234 break
235 }
236 }
237
238 baseMessage += `\n\n${suggestion}`
239
240 return baseMessage
241 }
242}
243
244// =============================================================================
245// Http Response Error
246// =============================================================================
247
248// =============================================================================
249// Supporting Schemas
250// =============================================================================
251
252/**
253 * Schema for provider-specific metadata which can be attached to error reasons.
254 *
255 * **Details**
256 *
257 * Provider-specific metadata is namespaced by provider name. Each provider
258 * value can contain arbitrary mutable JSON metadata or `null`.
259 *
260 * **Example** (Inspecting metadata shape)
261 *
262 * ```ts import.meta.vitest
263 * const metadata = {
264 * openai: {
265 * errorCode: "rate_limit_exceeded",
266 * requestId: "req_123"
267 * },
268 * anthropic: null
269 * }
270 *
271 * Array.of(metadata.openai.errorCode, metadata.anthropic) // => ["rate_limit_exceeded", null]
272 * ```
273 *
274 * @stability unstable
275 * @category schemas
276 * @since 4.0.0
277 */
278export const ProviderMetadata: Schema.$Record<
279 Schema.String,
280 Schema.NullOr<Schema.Codec<Schema.MutableJson>>
281> = Schema.Record(Schema.String, Schema.NullOr(Schema.MutableJson))
282
283/**
284 * Type of provider-specific metadata attached to AI error reasons.
285 *
286 * **Details**
287 *
288 * Metadata is keyed by provider name, and each provider value is either mutable
289 * JSON metadata or `null`.
290 *
291 * @stability unstable
292 * @category models
293 * @since 4.0.0
294 */
295export type ProviderMetadata = typeof ProviderMetadata.Type
296
297/**
298 * Provider-specific metadata attached to `RateLimitError`.
299 *
300 * @stability unstable
301 * @category configuration
302 * @since 4.0.0
303 */
304export interface RateLimitErrorMetadata extends ProviderMetadata {}
305
306/**
307 * Provider-specific metadata attached to `QuotaExhaustedError`.
308 *
309 * @stability unstable
310 * @category configuration
311 * @since 4.0.0
312 */
313export interface QuotaExhaustedErrorMetadata extends ProviderMetadata {}
314
315/**
316 * Provider-specific metadata attached to `AuthenticationError`.
317 *
318 * @stability unstable
319 * @category configuration
320 * @since 4.0.0
321 */
322export interface AuthenticationErrorMetadata extends ProviderMetadata {}
323
324/**
325 * Provider-specific metadata attached to `ContentPolicyError`.
326 *
327 * @stability unstable
328 * @category configuration
329 * @since 4.0.0
330 */
331export interface ContentPolicyErrorMetadata extends ProviderMetadata {}
332
333/**
334 * Provider-specific metadata attached to `InvalidRequestError`.
335 *
336 * @stability unstable
337 * @category configuration
338 * @since 4.0.0
339 */
340export interface InvalidRequestErrorMetadata extends ProviderMetadata {}
341
342/**
343 * Provider-specific metadata attached to `InternalProviderError`.
344 *
345 * @stability unstable
346 * @category configuration
347 * @since 4.0.0
348 */
349export interface InternalProviderErrorMetadata extends ProviderMetadata {}
350
351/**
352 * Provider-specific metadata attached to `InvalidOutputError`.
353 *
354 * @stability unstable
355 * @category configuration
356 * @since 4.0.0
357 */
358export interface InvalidOutputErrorMetadata extends ProviderMetadata {}
359
360/**
361 * Provider-specific metadata attached to `StructuredOutputError`.
362 *
363 * @stability unstable
364 * @category configuration
365 * @since 4.0.0
366 */
367export interface StructuredOutputErrorMetadata extends ProviderMetadata {}
368
369/**
370 * Provider-specific metadata attached to `UnsupportedSchemaError`.
371 *
372 * @stability unstable
373 * @category configuration
374 * @since 4.0.0
375 */
376export interface UnsupportedSchemaErrorMetadata extends ProviderMetadata {}
377
378/**
379 * Provider-specific metadata attached to `UnknownError`.
380 *
381 * @stability unstable
382 * @category configuration
383 * @since 4.0.0
384 */
385export interface UnknownErrorMetadata extends ProviderMetadata {}
386
387/**
388 * Schema for token usage information from AI operations.
389 *
390 * **Details**
391 *
392 * Schema for optional provider-reported token counts for prompt tokens,
393 * completion tokens, and total tokens.
394 *
395 * @stability unstable
396 * @category schemas
397 * @since 4.0.0
398 */
399export const UsageInfo = Schema.Struct({
400 promptTokens: Schema.optional(Schema.Int),
401 completionTokens: Schema.optional(Schema.Int),
402 totalTokens: Schema.optional(Schema.Int)
403}).annotate({ identifier: "UsageInfo" })
404
405/**
406 * Schema for the combined HTTP context used in error reporting.
407 *
408 * **When to use**
409 *
410 * Use to attach request details, optional response details, and optional body
411 * text to AI provider errors.
412 *
413 * **Details**
414 *
415 * Includes the required request details plus optional response details and raw
416 * response body.
417 *
418 * @see {@link HttpRequestDetails} for captured request details
419 * @see {@link HttpResponseDetails} for captured response details
420 *
421 * @stability unstable
422 * @category schemas
423 * @since 4.0.0
424 */
425export const HttpContext = Schema.Struct({
426 request: HttpRequestDetails,
427 response: Schema.optional(HttpResponseDetails),
428 body: Schema.optional(Schema.String)
429}).annotate({ identifier: "HttpContext" })
430
431/**
432 * Builds a description for an HTTP error returned by an AI provider.
433 *
434 * @stability unstable
435 * @category utilities
436 * @since 4.0.0
437 */
438export const buildErrorDescription = (params: {
439 readonly status: number
440 readonly message: string | undefined
441 readonly method: string
442 readonly url: string
443 readonly errorCode?: string | number | null | undefined
444 readonly errorType?: string | null | undefined
445 readonly requestId?: string | null | undefined
446 readonly body: string | undefined
447}): string => {
448 const parts: Array<string> = []
449
450 if (params.message) {
451 parts.push(params.message)
452 } else {
453 parts.push(`HTTP ${params.status}`)
454 }
455
456 parts.push(`(${params.method} ${params.url})`)
457
458 if (params.errorCode) {
459 parts.push(`[code: ${params.errorCode}]`)
460 } else if (params.errorType) {
461 parts.push(`[type: ${params.errorType}]`)
462 }
463
464 if (params.requestId) {
465 parts.push(`[requestId: ${params.requestId}]`)
466 }
467
468 if (!params.message && params.body) {
469 const truncated = params.body.length > 200
470 ? params.body.slice(0, 200) + "..."
471 : params.body
472 parts.push(`Response: ${truncated}`)
473 }
474
475 return parts.join(" ")
476}
477
478// =============================================================================
479// Reason Classes
480// =============================================================================
481
482/**
483 * Error indicating the request was rate limited.
484 *
485 * **Details**
486 *
487 * Rate limit errors are always retryable. When `retryAfter` is provided,
488 * callers should wait that duration before retrying.
489 *
490 * **Example** (Creating a rate limit error)
491 *
492 * ```ts import.meta.vitest
493 * import { Duration } from "effect"
494 * import { AiError } from "effect/ai"
495 *
496 * const rateLimitError = new AiError.RateLimitError({
497 * retryAfter: Duration.seconds(60)
498 * })
499 *
500 * const result = [rateLimitError._tag, rateLimitError.isRetryable] // => ["RateLimitError", true]
501 * ```
502 *
503 * @stability unstable
504 * @category errors
505 * @since 4.0.0
506 */
507export class RateLimitError extends Schema.Error<RateLimitError>(
508 "effect/ai/AiError/RateLimitError"
509)({
510 _tag: Schema.tag("RateLimitError"),
511 retryAfter: Schema.optional(Schema.Duration),
512 metadata: providerMetadataWithDefaults<RateLimitErrorMetadata>(),
513 http: Schema.optional(HttpContext)
514}) {
515 /**
516 * Marks `RateLimitError` as a semantic AI error reason for runtime guards.
517 *
518 * @stability unstable
519 * @since 4.0.0
520 */
521 readonly [ReasonTypeId] = ReasonTypeId
522
523 /**
524 * Rate limit errors are always retryable.
525 *
526 * @stability unstable
527 * @since 4.0.0
528 */
529 get isRetryable(): boolean {
530 return true
531 }
532
533 override get message(): string {
534 let msg = "Rate limit exceeded"
535 if (this.retryAfter) msg += `. Retry after ${Duration.format(this.retryAfter)}`
536 return msg
537 }
538}
539
540/**
541 * Error indicating account or billing limits have been reached.
542 *
543 * **Details**
544 *
545 * Quota exhausted errors are not retryable without user action.
546 *
547 * **Example** (Creating a quota exhausted error)
548 *
549 * ```ts import.meta.vitest
550 * import { AiError } from "effect/ai"
551 *
552 * const quotaError = new AiError.QuotaExhaustedError({})
553 *
554 * const result = [quotaError._tag, quotaError.isRetryable] // => ["QuotaExhaustedError", false]
555 * ```
556 *
557 * @stability unstable
558 * @category errors
559 * @since 4.0.0
560 */
561export class QuotaExhaustedError extends Schema.Error<QuotaExhaustedError>(
562 "effect/ai/AiError/QuotaExhaustedError"
563)({
564 _tag: Schema.tag("QuotaExhaustedError"),
565 resetAt: Schema.optional(Schema.DateTimeUtc),
566 metadata: providerMetadataWithDefaults<QuotaExhaustedErrorMetadata>(),
567 http: Schema.optional(HttpContext)
568}) {
569 /**
570 * Marks `QuotaExhaustedError` as a semantic AI error reason for runtime guards.
571 *
572 * @stability unstable
573 * @since 4.0.0
574 */
575 readonly [ReasonTypeId] = ReasonTypeId
576
577 /**
578 * Quota exhausted errors require user action and are not retryable.
579 *
580 * @stability unstable
581 * @since 4.0.0
582 */
583 get isRetryable(): boolean {
584 return false
585 }
586
587 override get message(): string {
588 let msg = "Quota exhausted"
589 if (this.resetAt) msg += `. Resets at ${this.resetAt}`
590 return `${msg}. Check your account billing and usage limits.`
591 }
592}
593
594/**
595 * Error indicating authentication or authorization failure.
596 *
597 * **Details**
598 *
599 * Authentication errors are never retryable without credential changes.
600 *
601 * **Example** (Creating an authentication error)
602 *
603 * ```ts import.meta.vitest
604 * import { AiError } from "effect/ai"
605 *
606 * const authError = new AiError.AuthenticationError({
607 * kind: "InvalidKey"
608 * })
609 *
610 * const result = [authError.kind, authError.isRetryable] // => ["InvalidKey", false]
611 *
612 * const detailed = new AiError.AuthenticationError({
613 * kind: "InsufficientPermissions",
614 * description: "Token expired"
615 * })
616 *
617 * detailed.message // => "InsufficientPermissions: Your API key lacks required permissions. Token expired"
618 * ```
619 *
620 * @stability unstable
621 * @category errors
622 * @since 4.0.0
623 */
624export class AuthenticationError extends Schema.Error<AuthenticationError>(
625 "effect/ai/AiError/AuthenticationError"
626)({
627 _tag: Schema.tag("AuthenticationError"),
628 kind: Schema.Literals(["InvalidKey", "ExpiredKey", "MissingKey", "InsufficientPermissions", "Unknown"]),
629 description: Schema.optional(Schema.String),
630 metadata: providerMetadataWithDefaults<AuthenticationErrorMetadata>(),
631 http: Schema.optional(HttpContext)
632}) {
633 /**
634 * Marks `AuthenticationError` as a semantic AI error reason for runtime guards.
635 *
636 * @stability unstable
637 * @since 4.0.0
638 */
639 readonly [ReasonTypeId] = ReasonTypeId
640
641 /**
642 * Authentication errors require credential changes and are not retryable.
643 *
644 * @stability unstable
645 * @since 4.0.0
646 */
647 get isRetryable(): boolean {
648 return false
649 }
650
651 override get message(): string {
652 const suggestions: Record<string, string> = {
653 InvalidKey: "Verify your API key is correct",
654 ExpiredKey: "Your API key has expired. Generate a new one",
655 MissingKey: "No API key provided. Set the appropriate environment variable",
656 InsufficientPermissions: "Your API key lacks required permissions",
657 Unknown: "Authentication failed. Check your credentials"
658 }
659 let msg = `${this.kind}: ${suggestions[this.kind]}`
660 if (this.description) msg += `. ${this.description}`
661 return msg
662 }
663}
664
665/**
666 * Error indicating content policy violation.
667 *
668 * **Details**
669 *
670 * Content policy errors are never retryable without content changes.
671 *
672 * **Example** (Creating a content policy error)
673 *
674 * ```ts import.meta.vitest
675 * import { AiError } from "effect/ai"
676 *
677 * const policyError = new AiError.ContentPolicyError({
678 * description: "Input contains prohibited content"
679 * })
680 *
681 * const result = [policyError.description, policyError.isRetryable] // => ["Input contains prohibited content", false]
682 * ```
683 *
684 * @stability unstable
685 * @category errors
686 * @since 4.0.0
687 */
688export class ContentPolicyError extends Schema.Error<ContentPolicyError>(
689 "effect/ai/AiError/ContentPolicyError"
690)({
691 _tag: Schema.tag("ContentPolicyError"),
692 description: Schema.String,
693 metadata: providerMetadataWithDefaults<ContentPolicyErrorMetadata>(),
694 http: Schema.optional(HttpContext)
695}) {
696 /**
697 * Marks `ContentPolicyError` as a semantic AI error reason for runtime guards.
698 *
699 * @stability unstable
700 * @since 4.0.0
701 */
702 readonly [ReasonTypeId] = ReasonTypeId
703
704 /**
705 * Content policy errors require content changes and are not retryable.
706 *
707 * @stability unstable
708 * @since 4.0.0
709 */
710 get isRetryable(): boolean {
711 return false
712 }
713
714 override get message(): string {
715 return `Content policy violation: ${this.description}`
716 }
717}
718
719/**
720 * Error indicating the request had invalid or malformed parameters.
721 *
722 * **Details**
723 *
724 * Invalid request errors require fixing the request and are not retryable.
725 *
726 * **Example** (Creating an invalid request error)
727 *
728 * ```ts import.meta.vitest
729 * import { AiError } from "effect/ai"
730 *
731 * const invalidRequestError = new AiError.InvalidRequestError({
732 * parameter: "temperature",
733 * constraint: "must be between 0 and 2",
734 * description: "Temperature value 5 is out of range"
735 * })
736 *
737 * const result = [invalidRequestError.parameter, invalidRequestError.isRetryable] // => ["temperature", false]
738 * ```
739 *
740 * @stability unstable
741 * @category errors
742 * @since 4.0.0
743 */
744export class InvalidRequestError extends Schema.Error<InvalidRequestError>(
745 "effect/ai/AiError/InvalidRequestError"
746)({
747 _tag: Schema.tag("InvalidRequestError"),
748 parameter: Schema.optional(Schema.String),
749 constraint: Schema.optional(Schema.String),
750 description: Schema.optional(Schema.String),
751 metadata: providerMetadataWithDefaults<InvalidRequestErrorMetadata>(),
752 http: Schema.optional(HttpContext)
753}) {
754 /**
755 * Marks `InvalidRequestError` as a semantic AI error reason for runtime guards.
756 *
757 * @stability unstable
758 * @since 4.0.0
759 */
760 readonly [ReasonTypeId] = ReasonTypeId
761
762 /**
763 * Invalid request errors require fixing the request and are not retryable.
764 *
765 * @stability unstable
766 * @since 4.0.0
767 */
768 get isRetryable(): boolean {
769 return false
770 }
771
772 override get message(): string {
773 let msg = "Invalid request"
774 if (this.parameter) msg += `: parameter '${this.parameter}'`
775 if (this.constraint) msg += ` ${this.constraint}`
776 if (this.description) msg += `. ${this.description}`
777 return msg
778 }
779}
780
781/**
782 * Error indicating the AI provider experienced an internal error.
783 *
784 * **Details**
785 *
786 * Internal provider errors are typically transient and are retryable.
787 *
788 * **Example** (Creating an internal provider error)
789 *
790 * ```ts import.meta.vitest
791 * import { AiError } from "effect/ai"
792 *
793 * const providerError = new AiError.InternalProviderError({
794 * description: "Server encountered an unexpected error"
795 * })
796 *
797 * const result = [providerError.description, providerError.isRetryable] // => ["Server encountered an unexpected error", true]
798 * ```
799 *
800 * @stability unstable
801 * @category errors
802 * @since 4.0.0
803 */
804export class InternalProviderError extends Schema.Error<InternalProviderError>(
805 "effect/ai/AiError/InternalProviderError"
806)({
807 _tag: Schema.tag("InternalProviderError"),
808 description: Schema.String,
809 metadata: providerMetadataWithDefaults<InternalProviderErrorMetadata>(),
810 http: Schema.optional(HttpContext)
811}) {
812 /**
813 * Marks `InternalProviderError` as a semantic AI error reason for runtime guards.
814 *
815 * @stability unstable
816 * @since 4.0.0
817 */
818 readonly [ReasonTypeId] = ReasonTypeId
819
820 /**
821 * Internal provider errors are typically transient and are retryable.
822 *
823 * @stability unstable
824 * @since 4.0.0
825 */
826 get isRetryable(): boolean {
827 return true
828 }
829
830 override get message(): string {
831 return `Internal provider error: ${this.description}`
832 }
833}
834
835/**
836 * Error indicating failure to parse or validate LLM output.
837 *
838 * **Details**
839 *
840 * Invalid output errors are retryable since LLM outputs are non-deterministic.
841 *
842 * **Example** (Creating an invalid output error)
843 *
844 * ```ts import.meta.vitest
845 * import { AiError } from "effect/ai"
846 *
847 * const parseError = new AiError.InvalidOutputError({
848 * description: "Expected a string but received a number"
849 * })
850 *
851 * const result = [parseError.description, parseError.isRetryable] // => ["Expected a string but received a number", true]
852 * ```
853 *
854 * @stability unstable
855 * @category errors
856 * @since 4.0.0
857 */
858export class InvalidOutputError extends Schema.Error<InvalidOutputError>(
859 "effect/ai/AiError/InvalidOutputError"
860)({
861 _tag: Schema.tag("InvalidOutputError"),
862 description: Schema.String,
863 metadata: providerMetadataWithDefaults<InvalidOutputErrorMetadata>(),
864 usage: Schema.optional(UsageInfo)
865}) {
866 /**
867 * Marks `InvalidOutputError` as a semantic AI error reason for runtime guards.
868 *
869 * @stability unstable
870 * @since 4.0.0
871 */
872 readonly [ReasonTypeId] = ReasonTypeId
873
874 /**
875 * Invalid output errors are retryable since LLM outputs are non-deterministic.
876 *
877 * @stability unstable
878 * @since 4.0.0
879 */
880 get isRetryable(): boolean {
881 return true
882 }
883
884 /**
885 * Creates an InvalidOutputError from a Schema error.
886 *
887 * **Example** (Creating an invalid output error from a schema error)
888 *
889 * ```ts import.meta.vitest
890 * import { Effect, Schema } from "effect"
891 * import { AiError } from "effect/ai"
892 *
893 * const schemaError = await Effect.runPromise(
894 * Schema.decodeUnknownEffect(Schema.Number)("not a number").pipe(Effect.flip)
895 * )
896 * const parseError = AiError.InvalidOutputError.fromSchemaError(schemaError)
897 * parseError.description // => "Expected number"
898 * ```
899 *
900 * @stability unstable
901 * @since 4.0.0
902 */
903 static fromSchemaError(error: Schema.SchemaError): InvalidOutputError {
904 return new InvalidOutputError({
905 description: error.message
906 })
907 }
908
909 override get message(): string {
910 return `Invalid output: ${this.description}`
911 }
912}
913
914/**
915 * Error indicating the LLM generated text that does not conform to the
916 * requested structured output schema.
917 *
918 * **Details**
919 *
920 * Structured output errors are retryable since LLM outputs are non-deterministic.
921 *
922 * **Example** (Creating a structured output error)
923 *
924 * ```ts import.meta.vitest
925 * import { AiError } from "effect/ai"
926 *
927 * const error = new AiError.StructuredOutputError({
928 * description: "Expected a valid JSON object",
929 * responseText: "{\"foo\":}"
930 * })
931 *
932 * const result = [error.description, error.responseText, error.isRetryable] // => ["Expected a valid JSON object", '{"foo":}', true]
933 * ```
934 *
935 * @stability unstable
936 * @category errors
937 * @since 4.0.0
938 */
939export class StructuredOutputError extends Schema.Error<StructuredOutputError>(
940 "effect/ai/AiError/StructuredOutputError"
941)({
942 _tag: Schema.tag("StructuredOutputError"),
943 description: Schema.String,
944 responseText: Schema.String,
945 metadata: providerMetadataWithDefaults<StructuredOutputErrorMetadata>(),
946 usage: Schema.optional(UsageInfo)
947}) {
948 /**
949 * Marks `StructuredOutputError` as a semantic AI error reason for runtime guards.
950 *
951 * @stability unstable
952 * @since 4.0.0
953 */
954 readonly [ReasonTypeId] = ReasonTypeId
955
956 /**
957 * Structured output errors are retryable since LLM outputs are non-deterministic.
958 *
959 * @stability unstable
960 * @since 4.0.0
961 */
962 get isRetryable(): boolean {
963 return true
964 }
965
966 /**
967 * Creates a StructuredOutputError from a Schema error.
968 *
969 * **Example** (Creating a structured output error from a schema error)
970 *
971 * ```ts import.meta.vitest
972 * import { Effect, Schema } from "effect"
973 * import { AiError } from "effect/ai"
974 *
975 * const schemaError = await Effect.runPromise(
976 * Schema.decodeUnknownEffect(Schema.Struct({ name: Schema.String }))({}).pipe(Effect.flip)
977 * )
978 * const parseError = AiError.StructuredOutputError.fromSchemaError(schemaError, "{}")
979 * parseError.responseText // => "{}"
980 * ```
981 *
982 * @stability unstable
983 * @since 4.0.0
984 */
985 static fromSchemaError(error: Schema.SchemaError, responseText: string): StructuredOutputError {
986 return new StructuredOutputError({
987 description: error.message,
988 responseText
989 })
990 }
991
992 override get message(): string {
993 return `Structured output validation failed: ${this.description}`
994 }
995}
996
997/**
998 * Error indicating a codec transformer rejected a schema because it contains
999 * unsupported constructs.
1000 *
1001 * **Details**
1002 *
1003 * Unsupported schema errors are not retryable because they indicate a
1004 * programmer error where the schema is incompatible with the provider.
1005 *
1006 * **Example** (Creating an unsupported schema error)
1007 *
1008 * ```ts import.meta.vitest
1009 * import { AiError } from "effect/ai"
1010 *
1011 * const error = new AiError.UnsupportedSchemaError({
1012 * description: "Unions are not supported in Anthropic structured output"
1013 * })
1014 *
1015 * const result = [error.description, error.isRetryable] // => ["Unions are not supported in Anthropic structured output", false]
1016 * ```
1017 *
1018 * @stability unstable
1019 * @category errors
1020 * @since 4.0.0
1021 */
1022export class UnsupportedSchemaError extends Schema.Error<UnsupportedSchemaError>(
1023 "effect/ai/AiError/UnsupportedSchemaError"
1024)({
1025 _tag: Schema.tag("UnsupportedSchemaError"),
1026 description: Schema.String,
1027 metadata: providerMetadataWithDefaults<UnsupportedSchemaErrorMetadata>()
1028}) {
1029 /**
1030 * Marks `UnsupportedSchemaError` as a semantic AI error reason for runtime guards.
1031 *
1032 * @stability unstable
1033 * @since 4.0.0
1034 */
1035 readonly [ReasonTypeId] = ReasonTypeId
1036
1037 /**
1038 * Unsupported schema errors are not retryable because they indicate a programmer error.
1039 *
1040 * @stability unstable
1041 * @since 4.0.0
1042 */
1043 get isRetryable(): boolean {
1044 return false
1045 }
1046
1047 override get message(): string {
1048 return `Unsupported schema: ${this.description}`
1049 }
1050}
1051
1052/**
1053 * Error data for unknown or unexpected AI failures.
1054 *
1055 * **Details**
1056 *
1057 * Unknown errors are not retryable by default since the cause is unknown.
1058 *
1059 * **Example** (Creating an unknown error)
1060 *
1061 * ```ts import.meta.vitest
1062 * import { AiError } from "effect/ai"
1063 *
1064 * const unknownError = new AiError.UnknownError({
1065 * description: "An unexpected error occurred"
1066 * })
1067 *
1068 * const result = [unknownError.description, unknownError.isRetryable] // => ["An unexpected error occurred", false]
1069 * ```
1070 *
1071 * @stability unstable
1072 * @category errors
1073 * @since 4.0.0
1074 */
1075export class UnknownError extends Schema.Error<UnknownError>(
1076 "effect/ai/AiError/UnknownError"
1077)({
1078 _tag: Schema.tag("UnknownError"),
1079 description: Schema.optional(Schema.String),
1080 metadata: providerMetadataWithDefaults<UnknownErrorMetadata>(),
1081 http: Schema.optional(HttpContext)
1082}) {
1083 /**
1084 * Marks `UnknownError` as a semantic AI error reason for runtime guards.
1085 *
1086 * @stability unstable
1087 * @since 4.0.0
1088 */
1089 readonly [ReasonTypeId] = ReasonTypeId
1090
1091 /**
1092 * Unknown errors are not retryable by default.
1093 *
1094 * @stability unstable
1095 * @since 4.0.0
1096 */
1097 get isRetryable(): boolean {
1098 return false
1099 }
1100
1101 override get message(): string {
1102 return this.description ?? "Unknown error"
1103 }
1104}
1105
1106// =============================================================================
1107// Tool Call Error Types
1108// =============================================================================
1109
1110/**
1111 * Error indicating the model requested a tool that doesn't exist in the toolkit.
1112 *
1113 * **Details**
1114 *
1115 * This error is retryable because the model may self-correct when provided
1116 * with the list of available tools.
1117 *
1118 * **Example** (Creating a tool not found error)
1119 *
1120 * ```ts import.meta.vitest
1121 * import { AiError } from "effect/ai"
1122 *
1123 * const error = new AiError.ToolNotFoundError({
1124 * toolName: "unknownTool",
1125 * availableTools: ["GetWeather", "GetTime"]
1126 * })
1127 *
1128 * const result = [error.toolName, error.availableTools, error.isRetryable] // => ["unknownTool", ["GetWeather", "GetTime"], true]
1129 * ```
1130 *
1131 * @stability unstable
1132 * @category errors
1133 * @since 4.0.0
1134 */
1135export class ToolNotFoundError extends Schema.Error<ToolNotFoundError>(
1136 "effect/ai/AiError/ToolNotFoundError"
1137)({
1138 _tag: Schema.tag("ToolNotFoundError"),
1139 toolName: Schema.String,
1140 availableTools: Schema.Array(Schema.String)
1141}) {
1142 /**
1143 * Marks `ToolNotFoundError` as a semantic AI error reason for runtime guards.
1144 *
1145 * @stability unstable
1146 * @since 4.0.0
1147 */
1148 readonly [ReasonTypeId] = ReasonTypeId
1149
1150 /**
1151 * Tool not found errors are retryable because the model may self-correct.
1152 *
1153 * @stability unstable
1154 * @since 4.0.0
1155 */
1156 get isRetryable(): boolean {
1157 return true
1158 }
1159
1160 override get message(): string {
1161 const availableTools = this.availableTools.length > 0 ? this.availableTools.join(", ") : "none"
1162 return `Tool '${this.toolName}' not found. Available tools: ${availableTools}`
1163 }
1164}
1165
1166/**
1167 * Error indicating the model's tool call parameters failed schema validation.
1168 *
1169 * **Details**
1170 *
1171 * This error is retryable because the model may correct its parameters
1172 * on subsequent attempts.
1173 *
1174 * **Example** (Creating a tool parameter validation error)
1175 *
1176 * ```ts import.meta.vitest
1177 * import { AiError } from "effect/ai"
1178 *
1179 * const error = new AiError.ToolParameterValidationError({
1180 * toolName: "GetWeather",
1181 * description: "Expected string, got number"
1182 * })
1183 *
1184 * const result = [error.toolName, error.description, error.isRetryable] // => ["GetWeather", "Expected string, got number", true]
1185 * ```
1186 *
1187 * @stability unstable
1188 * @category errors
1189 * @since 4.0.0
1190 */
1191export class ToolParameterValidationError extends Schema.Error<ToolParameterValidationError>(
1192 "effect/ai/AiError/ToolParameterValidationError"
1193)({
1194 _tag: Schema.tag("ToolParameterValidationError"),
1195 toolName: Schema.String,
1196 description: Schema.String
1197}) {
1198 /**
1199 * Marks `ToolParameterValidationError` as a semantic AI error reason for runtime guards.
1200 *
1201 * @stability unstable
1202 * @since 4.0.0
1203 */
1204 readonly [ReasonTypeId] = ReasonTypeId
1205
1206 /**
1207 * Parameter validation errors are retryable because the model may correct parameters.
1208 *
1209 * @stability unstable
1210 * @since 4.0.0
1211 */
1212 get isRetryable(): boolean {
1213 return true
1214 }
1215
1216 override get message(): string {
1217 return `Invalid parameters for tool '${this.toolName}': ${this.description}`
1218 }
1219}
1220
1221/**
1222 * Error indicating the tool handler returned an invalid result that does not
1223 * match the tool's schema.
1224 *
1225 * **Details**
1226 *
1227 * This error is not retryable because invalid results indicate a bug in the
1228 * tool handler implementation.
1229 *
1230 * **Example** (Creating an invalid tool result error)
1231 *
1232 * ```ts import.meta.vitest
1233 * import { AiError } from "effect/ai"
1234 *
1235 * const error = new AiError.InvalidToolResultError({
1236 * toolName: "GetWeather",
1237 * description: "Tool handler returned invalid result: missing 'temperature' field"
1238 * })
1239 *
1240 * const result = [error.toolName, error.isRetryable] // => ["GetWeather", false]
1241 * ```
1242 *
1243 * @stability unstable
1244 * @category errors
1245 * @since 4.0.0
1246 */
1247export class InvalidToolResultError extends Schema.Error<InvalidToolResultError>(
1248 "effect/ai/AiError/InvalidToolResultError"
1249)({
1250 _tag: Schema.tag("InvalidToolResultError"),
1251 toolName: Schema.String,
1252 description: Schema.String
1253}) {
1254 /**
1255 * Marks `InvalidToolResultError` as a semantic AI error reason for runtime guards.
1256 *
1257 * @stability unstable
1258 * @since 4.0.0
1259 */
1260 readonly [ReasonTypeId] = ReasonTypeId
1261
1262 /**
1263 * Invalid tool result errors are not retryable because they indicate a bug in the handler.
1264 *
1265 * @stability unstable
1266 * @since 4.0.0
1267 */
1268 get isRetryable(): boolean {
1269 return false
1270 }
1271
1272 override get message(): string {
1273 return `Tool '${this.toolName}' returned invalid result: ${this.description}`
1274 }
1275}
1276
1277/**
1278 * Error indicating the tool result cannot be encoded for sending back to the model.
1279 *
1280 * **Details**
1281 *
1282 * This error is not retryable because encoding failures indicate a bug in the
1283 * tool schema definitions.
1284 *
1285 * **Example** (Creating a tool result encoding error)
1286 *
1287 * ```ts import.meta.vitest
1288 * import { AiError } from "effect/ai"
1289 *
1290 * const error = new AiError.ToolResultEncodingError({
1291 * toolName: "GetWeather",
1292 * toolResult: { temperature: 72n },
1293 * description: "Cannot encode bigint values as JSON"
1294 * })
1295 *
1296 * const result = [error.toolName, error.description, error.isRetryable] // => ["GetWeather", "Cannot encode bigint values as JSON", false]
1297 * ```
1298 *
1299 * @stability unstable
1300 * @category errors
1301 * @since 4.0.0
1302 */
1303export class ToolResultEncodingError extends Schema.Error<ToolResultEncodingError>(
1304 "effect/ai/AiError/ToolResultEncodingError"
1305)({
1306 _tag: Schema.tag("ToolResultEncodingError"),
1307 toolName: Schema.String,
1308 toolResult: Schema.Unknown,
1309 description: Schema.String
1310}) {
1311 /**
1312 * Marks `ToolResultEncodingError` as a semantic AI error reason for runtime guards.
1313 *
1314 * @stability unstable
1315 * @since 4.0.0
1316 */
1317 readonly [ReasonTypeId] = ReasonTypeId
1318
1319 /**
1320 * Encoding errors are not retryable because they indicate a code bug.
1321 *
1322 * @stability unstable
1323 * @since 4.0.0
1324 */
1325 get isRetryable(): boolean {
1326 return false
1327 }
1328
1329 override get message(): string {
1330 return `Failed to encode result for tool '${this.toolName}': ${this.description}`
1331 }
1332}
1333
1334/**
1335 * Error indicating a provider-defined tool was configured with invalid arguments.
1336 *
1337 * **Details**
1338 *
1339 * This error is not retryable because it indicates a programming error in the
1340 * tool configuration that must be fixed in code.
1341 *
1342 * **Example** (Creating a tool configuration error)
1343 *
1344 * ```ts import.meta.vitest
1345 * import { AiError } from "effect/ai"
1346 *
1347 * const error = new AiError.ToolConfigurationError({
1348 * toolName: "OpenAiCodeInterpreter",
1349 * description: "Invalid container ID format"
1350 * })
1351 *
1352 * const result = [error.toolName, error.description, error.isRetryable] // => ["OpenAiCodeInterpreter", "Invalid container ID format", false]
1353 * ```
1354 *
1355 * @stability unstable
1356 * @category errors
1357 * @since 4.0.0
1358 */
1359export class ToolConfigurationError extends Schema.Error<ToolConfigurationError>(
1360 "effect/ai/AiError/ToolConfigurationError"
1361)({
1362 _tag: Schema.tag("ToolConfigurationError"),
1363 toolName: Schema.String,
1364 description: Schema.String
1365}) {
1366 /**
1367 * Marks `ToolConfigurationError` as a semantic AI error reason for runtime guards.
1368 *
1369 * @stability unstable
1370 * @since 4.0.0
1371 */
1372 readonly [ReasonTypeId] = ReasonTypeId
1373
1374 /**
1375 * Configuration errors are not retryable because they indicate a code bug.
1376 *
1377 * @stability unstable
1378 * @since 4.0.0
1379 */
1380 get isRetryable(): boolean {
1381 return false
1382 }
1383
1384 override get message(): string {
1385 return `Invalid configuration for tool '${this.toolName}': ${this.description}`
1386 }
1387}
1388
1389/**
1390 * Error indicating an operation requires a toolkit but none was provided.
1391 *
1392 * **Details**
1393 *
1394 * This error occurs when tool approval responses are present in the prompt
1395 * but no toolkit was provided to resolve them.
1396 *
1397 * **Example** (Creating a toolkit required error)
1398 *
1399 * ```ts import.meta.vitest
1400 * import { AiError } from "effect/ai"
1401 *
1402 * const error = new AiError.ToolkitRequiredError({
1403 * pendingApprovals: ["GetWeather", "SendEmail"]
1404 * })
1405 *
1406 * const result = [error.pendingApprovals, error.isRetryable] // => [["GetWeather", "SendEmail"], false]
1407 * ```
1408 *
1409 * @stability unstable
1410 * @category errors
1411 * @since 4.0.0
1412 */
1413export class ToolkitRequiredError extends Schema.Error<ToolkitRequiredError>(
1414 "effect/ai/AiError/ToolkitRequiredError"
1415)({
1416 _tag: Schema.tag("ToolkitRequiredError"),
1417 pendingApprovals: Schema.Array(Schema.String),
1418 description: Schema.optional(Schema.String)
1419}) {
1420 /**
1421 * Marks `ToolkitRequiredError` as a semantic AI error reason for runtime guards.
1422 *
1423 * @stability unstable
1424 * @since 4.0.0
1425 */
1426 readonly [ReasonTypeId] = ReasonTypeId
1427
1428 /**
1429 * Toolkit required errors are not retryable without providing a toolkit.
1430 *
1431 * @stability unstable
1432 * @since 4.0.0
1433 */
1434 get isRetryable(): boolean {
1435 return false
1436 }
1437
1438 override get message(): string {
1439 const tools = this.pendingApprovals.join(", ")
1440 return `Toolkit required to resolve pending tool approvals: ${tools}`
1441 }
1442}
1443
1444/**
1445 * Error indicating the user provided invalid input in their prompt.
1446 *
1447 * **Details**
1448 *
1449 * This error is raised when the prompt contains content that is structurally
1450 * valid but not supported by the provider (e.g., unsupported media types,
1451 * unsupported file formats, etc.).
1452 *
1453 * **Example** (Creating an invalid user input error)
1454 *
1455 * ```ts import.meta.vitest
1456 * import { AiError } from "effect/ai"
1457 *
1458 * const error = new AiError.InvalidUserInputError({
1459 * description: "Unsupported media type 'video/mp4'. Supported types include images, application/pdf, text/plain"
1460 * })
1461 *
1462 * const result = [error._tag, error.isRetryable] // => ["InvalidUserInputError", false]
1463 * ```
1464 *
1465 * @stability unstable
1466 * @category errors
1467 * @since 4.0.0
1468 */
1469export class InvalidUserInputError extends Schema.Error<InvalidUserInputError>(
1470 "effect/ai/AiError/InvalidUserInputError"
1471)({
1472 _tag: Schema.tag("InvalidUserInputError"),
1473 description: Schema.String
1474}) {
1475 /**
1476 * Marks `InvalidUserInputError` as a semantic AI error reason for runtime guards.
1477 *
1478 * @stability unstable
1479 * @since 4.0.0
1480 */
1481 readonly [ReasonTypeId] = ReasonTypeId
1482
1483 /**
1484 * Invalid user input errors require fixing the input and are not retryable.
1485 *
1486 * @stability unstable
1487 * @since 4.0.0
1488 */
1489 get isRetryable(): boolean {
1490 return false
1491 }
1492
1493 override get message(): string {
1494 return `Invalid user input: ${this.description}`
1495 }
1496}
1497
1498// =============================================================================
1499// AiErrorReason Union
1500// =============================================================================
1501
1502/**
1503 * Union type of all semantic error reasons that can occur during AI operations.
1504 *
1505 * **Details**
1506 *
1507 * Every reason carries a semantic `_tag`, a human-readable message, and an
1508 * `isRetryable` getter. Provider-facing reasons may also include retry timing,
1509 * provider metadata, usage information, or HTTP context.
1510 *
1511 * @stability unstable
1512 * @category errors
1513 * @since 4.0.0
1514 */
1515export type AiErrorReason =
1516 | RateLimitError
1517 | QuotaExhaustedError
1518 | AuthenticationError
1519 | ContentPolicyError
1520 | InvalidRequestError
1521 | InternalProviderError
1522 | NetworkError
1523 | InvalidOutputError
1524 | StructuredOutputError
1525 | UnsupportedSchemaError
1526 | UnknownError
1527 | ToolNotFoundError
1528 | ToolParameterValidationError
1529 | InvalidToolResultError
1530 | ToolResultEncodingError
1531 | ToolConfigurationError
1532 | ToolkitRequiredError
1533 | InvalidUserInputError
1534
1535/**
1536 * Schema for validating and parsing AI error reasons.
1537 *
1538 * **When to use**
1539 *
1540 * Use when decoding or validating unknown AI error reason values with Schema.
1541 *
1542 * **Details**
1543 *
1544 * This runtime schema is the union of the concrete AI error reason classes.
1545 *
1546 * @see {@link isAiErrorReason} for checking an existing value without Schema decoding
1547 *
1548 * @stability unstable
1549 * @category schemas
1550 * @since 4.0.0
1551 */
1552export const AiErrorReason: Schema.Union<[
1553 typeof RateLimitError,
1554 typeof QuotaExhaustedError,
1555 typeof AuthenticationError,
1556 typeof ContentPolicyError,
1557 typeof InvalidRequestError,
1558 typeof InternalProviderError,
1559 typeof NetworkError,
1560 typeof InvalidOutputError,
1561 typeof StructuredOutputError,
1562 typeof UnsupportedSchemaError,
1563 typeof UnknownError,
1564 typeof ToolNotFoundError,
1565 typeof ToolParameterValidationError,
1566 typeof InvalidToolResultError,
1567 typeof ToolResultEncodingError,
1568 typeof ToolConfigurationError,
1569 typeof ToolkitRequiredError,
1570 typeof InvalidUserInputError
1571]> = Schema.Union([
1572 RateLimitError,
1573 QuotaExhaustedError,
1574 AuthenticationError,
1575 ContentPolicyError,
1576 InvalidRequestError,
1577 InternalProviderError,
1578 NetworkError,
1579 InvalidOutputError,
1580 StructuredOutputError,
1581 UnsupportedSchemaError,
1582 UnknownError,
1583 ToolNotFoundError,
1584 ToolParameterValidationError,
1585 InvalidToolResultError,
1586 ToolResultEncodingError,
1587 ToolConfigurationError,
1588 ToolkitRequiredError,
1589 InvalidUserInputError
1590])
1591
1592// =============================================================================
1593// Top-Level AiError
1594// =============================================================================
1595
1596const TypeId = "~effect/ai/AiError" as const
1597
1598/**
1599 * Schema for the top-level AI error wrapper using the `reason` pattern.
1600 *
1601 * **When to use**
1602 *
1603 * Use when you need AI errors that can be handled by semantic reason with
1604 * `Effect.catchReason`.
1605 *
1606 * **Details**
1607 *
1608 * This error stores `module` and `method` context, the semantic `reason`, and
1609 * delegates `isRetryable` and `retryAfter` to the underlying reason.
1610 *
1611 * **Example** (Handling an AI error by tag)
1612 *
1613 * ```ts import.meta.vitest
1614 * import { Duration, Effect } from "effect"
1615 * import { AiError } from "effect/ai"
1616 *
1617 * const aiOperation = Effect.fail(new AiError.AiError({
1618 * module: "OpenAI",
1619 * method: "generateText",
1620 * reason: new AiError.RateLimitError({ retryAfter: Duration.seconds(30) })
1621 * }))
1622 *
1623 * // Handle specific reason types
1624 * const handled = aiOperation.pipe(
1625 * Effect.catchTag("AiError", (error) => {
1626 * if (error.reason._tag === "RateLimitError") {
1627 * return Effect.succeed(`Retry after ${error.retryAfter}`)
1628 * }
1629 * return Effect.fail(error)
1630 * })
1631 * )
1632 *
1633 * await Effect.runPromise(handled) // => "Retry after 30000 millis"
1634 * ```
1635 *
1636 * @stability unstable
1637 * @category schemas
1638 * @since 4.0.0
1639 */
1640export class AiError extends Schema.Error<AiError>(
1641 "effect/ai/AiError/AiError"
1642)({
1643 _tag: Schema.tag("AiError"),
1644 module: Schema.String,
1645 method: Schema.String,
1646 reason: AiErrorReason
1647}) {
1648 readonly [TypeId] = TypeId
1649 override readonly cause = this.reason
1650
1651 /**
1652 * Delegates to the underlying reason's `isRetryable` getter.
1653 *
1654 * @stability unstable
1655 * @since 4.0.0
1656 */
1657 get isRetryable(): boolean {
1658 return this.reason.isRetryable
1659 }
1660
1661 /**
1662 * Delegates to the underlying reason's `retryAfter` if present.
1663 *
1664 * @stability unstable
1665 * @since 4.0.0
1666 */
1667 get retryAfter(): Duration.Duration | undefined {
1668 return "retryAfter" in this.reason ? this.reason.retryAfter : undefined
1669 }
1670
1671 override get message(): string {
1672 return `${this.module}.${this.method}: ${this.reason.message}`
1673 }
1674}
1675
1676/**
1677 * The encoded (serialized) form of an `AiError`.
1678 *
1679 * @stability unstable
1680 * @category schemas
1681 * @since 4.0.0
1682 */
1683export type AiErrorEncoded = typeof AiError["Encoded"]
1684
1685/**
1686 * Type guard to check if a value is an `AiError`.
1687 *
1688 * **Example** (Checking for an AI error)
1689 *
1690 * ```ts import.meta.vitest
1691 * import { AiError } from "effect/ai"
1692 *
1693 * const someError = new Error("generic error")
1694 * const aiError = AiError.make({
1695 * module: "Test",
1696 * method: "example",
1697 * reason: new AiError.RateLimitError({})
1698 * })
1699 *
1700 * const result = [AiError.isAiError(someError), AiError.isAiError(aiError)] // => [false, true]
1701 * ```
1702 *
1703 * @stability unstable
1704 * @category guards
1705 * @since 4.0.0
1706 */
1707export const isAiError = (u: unknown): u is AiError => Predicate.hasProperty(u, TypeId)
1708
1709/**
1710 * Type guard to check if a value is an `AiErrorReason`.
1711 *
1712 * **Example** (Checking for an AI error reason)
1713 *
1714 * ```ts import.meta.vitest
1715 * import { AiError } from "effect/ai"
1716 *
1717 * const rateLimitError = new AiError.RateLimitError({})
1718 * const genericError = new Error("generic error")
1719 *
1720 * const result = [AiError.isAiErrorReason(rateLimitError), AiError.isAiErrorReason(genericError)] // => [true, false]
1721 * ```
1722 *
1723 * @stability unstable
1724 * @category guards
1725 * @since 4.0.0
1726 */
1727export const isAiErrorReason = (u: unknown): u is AiErrorReason => Predicate.hasProperty(u, ReasonTypeId)
1728
1729/**
1730 * Creates an `AiError` with the given reason.
1731 *
1732 * **Example** (Creating an AI error)
1733 *
1734 * ```ts import.meta.vitest
1735 * import { Duration } from "effect"
1736 * import { AiError } from "effect/ai"
1737 *
1738 * const error = AiError.make({
1739 * module: "OpenAI",
1740 * method: "completion",
1741 * reason: new AiError.RateLimitError({
1742 * retryAfter: Duration.seconds(60)
1743 * })
1744 * })
1745 *
1746 * const result = [error.module, error.method, error.reason._tag] // => ["OpenAI", "completion", "RateLimitError"]
1747 * ```
1748 *
1749 * @stability unstable
1750 * @category constructors
1751 * @since 4.0.0
1752 */
1753export const make = (params: {
1754 readonly module: string
1755 readonly method: string
1756 readonly reason: AiErrorReason
1757}): AiError => new AiError(params)
1758
1759/**
1760 * Maps HTTP status codes to semantic error reasons.
1761 *
1762 * **When to use**
1763 *
1764 * Use as the base mapping when provider packages translate HTTP status codes into
1765 * provider-specific error reasons.
1766 *
1767 * **Example** (Mapping an HTTP status to a reason)
1768 *
1769 * ```ts import.meta.vitest
1770 * import { AiError } from "effect/ai"
1771 *
1772 * const reason = AiError.reasonFromHttpStatus({
1773 * status: 429,
1774 * body: { error: "Rate limit exceeded" }
1775 * })
1776 *
1777 * reason._tag // => "RateLimitError"
1778 * ```
1779 *
1780 * @stability unstable
1781 * @category constructors
1782 * @since 4.0.0
1783 */
1784export const reasonFromHttpStatus = (params: {
1785 readonly status: number
1786 readonly body?: unknown
1787 readonly http?: typeof HttpContext.Type
1788 readonly metadata?: ProviderMetadata
1789 readonly description?: string | undefined
1790}): AiErrorReason => {
1791 const { status, http, metadata, description } = params
1792 const common = {
1793 http,
1794 ...(metadata ? { metadata } : undefined),
1795 ...(description ? { description } : undefined)
1796 }
1797 switch (status) {
1798 case 400:
1799 return new InvalidRequestError(common)
1800 case 401:
1801 return new AuthenticationError({ kind: "InvalidKey", ...common })
1802 case 403:
1803 return new AuthenticationError({ kind: "InsufficientPermissions", ...common })
1804 case 429:
1805 return new RateLimitError(common)
1806 default:
1807 if (status >= 500) {
1808 return new InternalProviderError({ description: "Server error", ...common })
1809 }
1810 return new UnknownError(common)
1811 }
1812}