import { assert, describe, it } from "@effect/vitest" import { Duration, Effect, Schema } from "effect" import { AiError } from "effect/unstable/ai" describe("AiError", () => { describe("reason types", () => { describe("RateLimitError", () => { it("should be retryable", () => { const error = new AiError.RateLimitError({}) assert.isTrue(error.isRetryable) }) it("should format message with retryAfter", () => { const error = new AiError.RateLimitError({ retryAfter: Duration.seconds(60) }) assert.match(error.message, /Retry after/) }) it("should have _tag set correctly", () => { const error = new AiError.RateLimitError({}) assert.strictEqual(error._tag, "RateLimitError") }) }) describe("QuotaExhaustedError", () => { it("should not be retryable", () => { const error = new AiError.QuotaExhaustedError({}) assert.isFalse(error.isRetryable) }) it("should format message correctly", () => { const error = new AiError.QuotaExhaustedError({}) assert.match(error.message, /Quota exhausted/) }) it("should have _tag set correctly", () => { const error = new AiError.QuotaExhaustedError({}) assert.strictEqual(error._tag, "QuotaExhaustedError") }) }) describe("AuthenticationError", () => { it("should not be retryable", () => { const error = new AiError.AuthenticationError({ kind: "InvalidKey" }) assert.isFalse(error.isRetryable) }) it("should format message based on kind", () => { const invalidKey = new AiError.AuthenticationError({ kind: "InvalidKey" }) assert.match(invalidKey.message, /InvalidKey/) const expiredKey = new AiError.AuthenticationError({ kind: "ExpiredKey" }) assert.match(expiredKey.message, /ExpiredKey/) const missingKey = new AiError.AuthenticationError({ kind: "MissingKey" }) assert.match(missingKey.message, /MissingKey/) const insufficientPermissions = new AiError.AuthenticationError({ kind: "InsufficientPermissions" }) assert.match(insufficientPermissions.message, /InsufficientPermissions/) }) it("should have _tag set correctly", () => { const error = new AiError.AuthenticationError({ kind: "Unknown" }) assert.strictEqual(error._tag, "AuthenticationError") }) }) describe("ContentPolicyError", () => { it("should not be retryable", () => { const error = new AiError.ContentPolicyError({ description: "hate speech detected" }) assert.isFalse(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.ContentPolicyError({ description: "Input contains prohibited content" }) assert.match(error.message, /Content policy violation/) assert.match(error.message, /Input contains prohibited content/) }) it("should have _tag set correctly", () => { const error = new AiError.ContentPolicyError({ description: "violation" }) assert.strictEqual(error._tag, "ContentPolicyError") }) }) describe("InvalidRequestError", () => { it("should not be retryable", () => { const error = new AiError.InvalidRequestError({ parameter: "temperature", constraint: "must be between 0 and 2" }) assert.isFalse(error.isRetryable) }) it("should format message with parameter details", () => { const error = new AiError.InvalidRequestError({ parameter: "temperature", constraint: "must be between 0 and 2", description: "Value 5 is invalid" }) assert.match(error.message, /parameter 'temperature'/) assert.match(error.message, /must be between 0 and 2/) assert.match(error.message, /Value 5 is invalid/) }) it("should have _tag set correctly", () => { const error = new AiError.InvalidRequestError({}) assert.strictEqual(error._tag, "InvalidRequestError") }) }) describe("InternalProviderError", () => { it("should be retryable", () => { const error = new AiError.InternalProviderError({ description: "Server error" }) assert.isTrue(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.InternalProviderError({ description: "Unexpected server failure" }) assert.match(error.message, /Internal provider error/) assert.match(error.message, /Unexpected server failure/) }) it("should have _tag set correctly", () => { const error = new AiError.InternalProviderError({ description: "Server error" }) assert.strictEqual(error._tag, "InternalProviderError") }) }) describe("InvalidOutputError", () => { it("should be retryable", () => { const error = new AiError.InvalidOutputError({ description: "Invalid JSON structure" }) assert.isTrue(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.InvalidOutputError({ description: "Expected string but got number" }) assert.match(error.message, /Invalid output/) assert.match(error.message, /Expected string but got number/) }) it.effect("should create from SchemaError", () => Effect.gen(function*() { const TestSchema = Schema.Struct({ name: Schema.String }) const result = yield* Effect.exit( Schema.decodeUnknownEffect(TestSchema)({ name: 123 }) ) if (result._tag === "Failure") { const cause = result.cause if ("error" in cause && Schema.isSchemaError(cause.error)) { const parseError = AiError.InvalidOutputError.fromSchemaError(cause.error) assert.strictEqual(parseError._tag, "InvalidOutputError") assert.isString(parseError.description) } } })) it("should have _tag set correctly", () => { const error = new AiError.InvalidOutputError({ description: "Test error" }) assert.strictEqual(error._tag, "InvalidOutputError") }) }) describe("UnknownError", () => { it("should not be retryable", () => { const error = new AiError.UnknownError({}) assert.isFalse(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.UnknownError({ description: "Something unexpected happened" }) assert.strictEqual(error.message, "Something unexpected happened") }) it("should use default message without description", () => { const error = new AiError.UnknownError({}) assert.strictEqual(error.message, "Unknown error") }) it("should have _tag set correctly", () => { const error = new AiError.UnknownError({}) assert.strictEqual(error._tag, "UnknownError") }) }) describe("ToolNotFoundError", () => { it("should be retryable", () => { const error = new AiError.ToolNotFoundError({ toolName: "UnknownTool", availableTools: ["GetWeather", "GetTime"] }) assert.isTrue(error.isRetryable) }) it("should format message with available tools", () => { const error = new AiError.ToolNotFoundError({ toolName: "UnknownTool", availableTools: ["GetWeather", "GetTime"] }) assert.match(error.message, /Tool 'UnknownTool' not found/) assert.match(error.message, /Available tools: GetWeather, GetTime/) }) it("should format message with no available tools", () => { const error = new AiError.ToolNotFoundError({ toolName: "UnknownTool", availableTools: [] }) assert.match(error.message, /Available tools: none/) }) it("should have _tag set correctly", () => { const error = new AiError.ToolNotFoundError({ toolName: "Test", availableTools: [] }) assert.strictEqual(error._tag, "ToolNotFoundError") }) }) describe("ToolParameterValidationError", () => { it("should be retryable", () => { const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: { location: "NYC" }, description: "Expected string" }) assert.isTrue(error.isRetryable) }) it("should format message with tool name", () => { const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: { location: "NYC" }, description: "Expected string" }) assert.match(error.message, /Invalid parameters for tool 'GetWeather'/) }) it("should format message with validation message", () => { const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: { location: 123 }, description: "Expected string, got number" }) assert.match(error.message, /Expected string, got number/) }) it("should store tool params", () => { const params = { location: 123 } const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: params, description: "Expected string" }) assert.deepStrictEqual(error.toolParams, params) }) it("should have _tag set correctly", () => { const error = new AiError.ToolParameterValidationError({ toolName: "Test", toolParams: {}, description: "Error" }) assert.strictEqual(error._tag, "ToolParameterValidationError") }) }) describe("InvalidToolResultError", () => { it("should not be retryable", () => { const error = new AiError.InvalidToolResultError({ toolName: "GetWeather", description: "missing required field" }) assert.isFalse(error.isRetryable) }) it("should format message with tool name", () => { const error = new AiError.InvalidToolResultError({ toolName: "GetWeather", description: "missing required field" }) assert.match(error.message, /Tool 'GetWeather' returned invalid result/) }) it("should format message with description", () => { const error = new AiError.InvalidToolResultError({ toolName: "GetWeather", description: "missing 'temperature' field" }) assert.match(error.message, /missing 'temperature' field/) }) it("should have _tag set correctly", () => { const error = new AiError.InvalidToolResultError({ toolName: "Test", description: "invalid result" }) assert.strictEqual(error._tag, "InvalidToolResultError") }) }) describe("ToolResultEncodingError", () => { it("should not be retryable", () => { const error = new AiError.ToolResultEncodingError({ toolName: "GetWeather", toolResult: { temp: 72 }, description: "Cannot encode" }) assert.isFalse(error.isRetryable) }) it("should format message with tool name", () => { const error = new AiError.ToolResultEncodingError({ toolName: "GetWeather", toolResult: { temp: 72 }, description: "Cannot encode" }) assert.match(error.message, /Failed to encode result for tool 'GetWeather'/) }) it("should format message with validation message", () => { const error = new AiError.ToolResultEncodingError({ toolName: "GetWeather", toolResult: { circular: "ref" }, description: "Cannot encode circular reference" }) assert.match(error.message, /Cannot encode circular reference/) }) it("should have _tag set correctly", () => { const error = new AiError.ToolResultEncodingError({ toolName: "Test", toolResult: {}, description: "Error" }) assert.strictEqual(error._tag, "ToolResultEncodingError") }) }) describe("ToolConfigurationError", () => { it("should not be retryable", () => { const error = new AiError.ToolConfigurationError({ toolName: "OpenAiCodeInterpreter", description: "Invalid container ID format" }) assert.isFalse(error.isRetryable) }) it("should format message with tool name", () => { const error = new AiError.ToolConfigurationError({ toolName: "OpenAiCodeInterpreter", description: "Invalid container ID format" }) assert.match(error.message, /Invalid configuration for tool 'OpenAiCodeInterpreter'/) }) it("should format message with description", () => { const error = new AiError.ToolConfigurationError({ toolName: "OpenAiWebSearch", description: "search_context_size must be between 1 and 10" }) assert.match(error.message, /search_context_size must be between 1 and 10/) }) it("should have _tag set correctly", () => { const error = new AiError.ToolConfigurationError({ toolName: "Test", description: "Error" }) assert.strictEqual(error._tag, "ToolConfigurationError") }) }) describe("StructuredOutputError", () => { it("should be retryable", () => { const error = new AiError.StructuredOutputError({ description: "Invalid JSON structure", responseText: "{\"invalid\":}" }) assert.isTrue(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.StructuredOutputError({ description: "Expected a valid JSON object", responseText: "{\"test\":true}" }) assert.match(error.message, /Structured output validation failed/) assert.match(error.message, /Expected a valid JSON object/) }) it("should have _tag set correctly", () => { const error = new AiError.StructuredOutputError({ description: "Test error", responseText: "{\"foo\":\"bar\"}" }) assert.strictEqual(error._tag, "StructuredOutputError") }) it.effect("should create from SchemaError", () => Effect.gen(function*() { const TestSchema = Schema.Struct({ name: Schema.String }) const result = yield* Effect.exit( Schema.decodeUnknownEffect(TestSchema)({ name: 123 }) ) if (result._tag === "Failure") { const cause = result.cause if ("error" in cause && Schema.isSchemaError(cause.error)) { const parseError = AiError.StructuredOutputError.fromSchemaError(cause.error, "{\"name\":123}") assert.strictEqual(parseError._tag, "StructuredOutputError") assert.isString(parseError.description) assert.strictEqual(parseError.responseText, "{\"name\":123}") } } })) }) describe("UnsupportedSchemaError", () => { it("should not be retryable", () => { const error = new AiError.UnsupportedSchemaError({ description: "Unions are not supported" }) assert.isFalse(error.isRetryable) }) it("should format message with description", () => { const error = new AiError.UnsupportedSchemaError({ description: "Unions are not supported in Anthropic structured output" }) assert.match(error.message, /Unsupported schema/) assert.match(error.message, /Unions are not supported in Anthropic structured output/) }) it("should have _tag set correctly", () => { const error = new AiError.UnsupportedSchemaError({ description: "Test error" }) assert.strictEqual(error._tag, "UnsupportedSchemaError") }) }) }) describe("delegation", () => { it("should delegate isRetryable to reason", () => { const retryableError = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.RateLimitError({}) }) assert.isTrue(retryableError.isRetryable) const nonRetryableError = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.AuthenticationError({ kind: "InvalidKey" }) }) assert.isFalse(nonRetryableError.isRetryable) }) it("should delegate retryAfter to reason", () => { const errorWithRetryAfter = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.RateLimitError({ retryAfter: Duration.seconds(60) }) }) assert.isDefined(errorWithRetryAfter.retryAfter) const errorWithoutRetryAfter = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.AuthenticationError({ kind: "InvalidKey" }) }) assert.isUndefined(errorWithoutRetryAfter.retryAfter) }) it("should format message with module, method, and reason", () => { const error = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.RateLimitError({}) }) assert.match(error.message, /OpenAI\.completion:/) assert.match(error.message, /Rate limit exceeded/) }) it("should have _tag set correctly", () => { const error = new AiError.AiError({ module: "Test", method: "test", reason: new AiError.UnknownError({}) }) assert.strictEqual(error._tag, "AiError") }) }) describe("constructors", () => { describe("make", () => { it("should create AiError", () => { const error = AiError.make({ module: "OpenAI", method: "completion", reason: new AiError.RateLimitError({}) }) assert.strictEqual(error._tag, "AiError") assert.strictEqual(error.module, "OpenAI") assert.strictEqual(error.method, "completion") assert.strictEqual(error.reason._tag, "RateLimitError") }) }) describe("reasonFromHttpStatus", () => { it("should map 400 to InvalidRequestError", () => { const reason = AiError.reasonFromHttpStatus({ status: 400 }) assert.strictEqual(reason._tag, "InvalidRequestError") }) it("should map 401 to AuthenticationError with InvalidKey", () => { const reason = AiError.reasonFromHttpStatus({ status: 401 }) assert.strictEqual(reason._tag, "AuthenticationError") if (reason._tag === "AuthenticationError") { assert.strictEqual(reason.kind, "InvalidKey") } }) it("should map 403 to AuthenticationError with InsufficientPermissions", () => { const reason = AiError.reasonFromHttpStatus({ status: 403 }) assert.strictEqual(reason._tag, "AuthenticationError") if (reason._tag === "AuthenticationError") { assert.strictEqual(reason.kind, "InsufficientPermissions") } }) it("should map 429 to RateLimitError", () => { const reason = AiError.reasonFromHttpStatus({ status: 429 }) assert.strictEqual(reason._tag, "RateLimitError") }) it("should map 5xx to InternalProviderError", () => { assert.strictEqual( AiError.reasonFromHttpStatus({ status: 500 })._tag, "InternalProviderError" ) assert.strictEqual( AiError.reasonFromHttpStatus({ status: 502 })._tag, "InternalProviderError" ) assert.strictEqual( AiError.reasonFromHttpStatus({ status: 503 })._tag, "InternalProviderError" ) }) it("should map unknown status to UnknownError", () => { const reason = AiError.reasonFromHttpStatus({ status: 418 }) assert.strictEqual(reason._tag, "UnknownError") }) }) }) describe("type guards", () => { describe("isAiError", () => { it("should return true for AiError", () => { const error = new AiError.AiError({ module: "Test", method: "test", reason: new AiError.UnknownError({}) }) assert.isTrue(AiError.isAiError(error)) }) it("should return false for non-AiError values", () => { assert.isFalse(AiError.isAiError(new Error("regular error"))) assert.isFalse(AiError.isAiError(null)) assert.isFalse(AiError.isAiError(undefined)) assert.isFalse(AiError.isAiError({ _tag: "FakeError" })) }) it("should return false for reason types", () => { const networkError = new AiError.NetworkError({ reason: "TransportError", request: { method: "GET", url: "https://example.com", urlParams: [], hash: undefined, headers: {} } }) assert.isFalse(AiError.isAiError(networkError)) }) }) }) describe("supporting schemas", () => { describe("ProviderMetadata", () => { it.effect("should encode and decode roundtrip", () => Effect.gen(function*() { const metadata = { name: "OpenAI", errorCode: "rate_limit_exceeded", errorType: "rate_limit", requestId: "req_123abc" } const encoded = yield* Schema.encodeEffect(AiError.ProviderMetadata)(metadata) const decoded = yield* Schema.decodeEffect(AiError.ProviderMetadata)(encoded) assert.deepStrictEqual(decoded, metadata) })) }) describe("UsageInfo", () => { it.effect("should encode and decode roundtrip", () => Effect.gen(function*() { const usage = { promptTokens: 100, completionTokens: 50, totalTokens: 150 } const encoded = yield* Schema.encodeEffect(AiError.UsageInfo)(usage) const decoded = yield* Schema.decodeEffect(AiError.UsageInfo)(encoded) assert.deepStrictEqual(decoded, usage) })) }) describe("HttpContext", () => { it.effect("should encode and decode roundtrip", () => Effect.gen(function*() { const context = { request: { method: "POST" as const, url: "https://api.example.com/v1/chat", urlParams: [["model", "gpt-4"]] as Array<[string, string]>, hash: undefined, headers: { "Content-Type": "application/json" } }, response: { status: 200, headers: { "Content-Type": "application/json" } }, body: "{\"result\": \"success\"}" } const encoded = yield* Schema.encodeEffect(AiError.HttpContext)(context) const decoded = yield* Schema.decodeEffect(AiError.HttpContext)(encoded) assert.deepStrictEqual(decoded, context) })) }) }) describe("schema roundtrip", () => { it.effect("RateLimitError roundtrip", () => Effect.gen(function*() { const error = new AiError.RateLimitError({ retryAfter: Duration.seconds(60) }) const encoded = yield* Schema.encodeEffect(AiError.RateLimitError)(error) const decoded = yield* Schema.decodeEffect(AiError.RateLimitError)(encoded) assert.strictEqual(decoded._tag, "RateLimitError") assert.isDefined(decoded.retryAfter) })) it.effect("AiErrorReason union roundtrip", () => Effect.gen(function*() { const rateLimitError: AiError.AiErrorReason = new AiError.RateLimitError({}) const encoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(rateLimitError) const decoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(encoded) assert.strictEqual(decoded._tag, "RateLimitError") const authError: AiError.AiErrorReason = new AiError.AuthenticationError({ kind: "ExpiredKey" }) const authEncoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(authError) const authDecoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(authEncoded) assert.strictEqual(authDecoded._tag, "AuthenticationError") })) it.effect("AiError roundtrip", () => Effect.gen(function*() { const error = new AiError.AiError({ module: "OpenAI", method: "completion", reason: new AiError.RateLimitError({}) }) const encoded = yield* Schema.encodeEffect(AiError.AiError)(error) const decoded = yield* Schema.decodeEffect(AiError.AiError)(encoded) assert.strictEqual(decoded._tag, "AiError") assert.strictEqual(decoded.module, "OpenAI") assert.strictEqual(decoded.method, "completion") assert.strictEqual(decoded.reason._tag, "RateLimitError") })) it.effect("ToolNotFoundError roundtrip", () => Effect.gen(function*() { const error = new AiError.ToolNotFoundError({ toolName: "UnknownTool", availableTools: ["GetWeather", "GetTime"] }) const encoded = yield* Schema.encodeEffect(AiError.ToolNotFoundError)(error) const decoded = yield* Schema.decodeEffect(AiError.ToolNotFoundError)(encoded) assert.strictEqual(decoded._tag, "ToolNotFoundError") assert.strictEqual(decoded.toolName, "UnknownTool") assert.deepStrictEqual(decoded.availableTools, ["GetWeather", "GetTime"]) })) it.effect("ToolParameterValidationError roundtrip", () => Effect.gen(function*() { const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: { location: 123 }, description: "Expected string" }) const encoded = yield* Schema.encodeEffect(AiError.ToolParameterValidationError)(error) const decoded = yield* Schema.decodeEffect(AiError.ToolParameterValidationError)(encoded) assert.strictEqual(decoded._tag, "ToolParameterValidationError") assert.strictEqual(decoded.toolName, "GetWeather") assert.strictEqual(decoded.description, "Expected string") })) it.effect("InvalidToolResultError roundtrip", () => Effect.gen(function*() { const error = new AiError.InvalidToolResultError({ toolName: "GetWeather", description: "missing required field" }) const encoded = yield* Schema.encodeEffect(AiError.InvalidToolResultError)(error) const decoded = yield* Schema.decodeEffect(AiError.InvalidToolResultError)(encoded) assert.strictEqual(decoded._tag, "InvalidToolResultError") assert.strictEqual(decoded.toolName, "GetWeather") assert.strictEqual(decoded.description, "missing required field") })) it.effect("ToolResultEncodingError roundtrip", () => Effect.gen(function*() { const error = new AiError.ToolResultEncodingError({ toolName: "GetWeather", toolResult: { temp: 72 }, description: "Circular reference" }) const encoded = yield* Schema.encodeEffect(AiError.ToolResultEncodingError)(error) const decoded = yield* Schema.decodeEffect(AiError.ToolResultEncodingError)(encoded) assert.strictEqual(decoded._tag, "ToolResultEncodingError") assert.strictEqual(decoded.toolName, "GetWeather") assert.strictEqual(decoded.description, "Circular reference") })) it.effect("ToolConfigurationError roundtrip", () => Effect.gen(function*() { const error = new AiError.ToolConfigurationError({ toolName: "OpenAiCodeInterpreter", description: "Invalid container ID format" }) const encoded = yield* Schema.encodeEffect(AiError.ToolConfigurationError)(error) const decoded = yield* Schema.decodeEffect(AiError.ToolConfigurationError)(encoded) assert.strictEqual(decoded._tag, "ToolConfigurationError") assert.strictEqual(decoded.toolName, "OpenAiCodeInterpreter") assert.strictEqual(decoded.description, "Invalid container ID format") })) it.effect("StructuredOutputError roundtrip", () => Effect.gen(function*() { const error = new AiError.StructuredOutputError({ description: "Invalid JSON structure", responseText: "{\"invalid\":}" }) const encoded = yield* Schema.encodeEffect(AiError.StructuredOutputError)(error) const decoded = yield* Schema.decodeEffect(AiError.StructuredOutputError)(encoded) assert.strictEqual(decoded._tag, "StructuredOutputError") assert.strictEqual(decoded.description, "Invalid JSON structure") assert.strictEqual(decoded.responseText, "{\"invalid\":}") })) it.effect("UnsupportedSchemaError roundtrip", () => Effect.gen(function*() { const error = new AiError.UnsupportedSchemaError({ description: "Unions are not supported" }) const encoded = yield* Schema.encodeEffect(AiError.UnsupportedSchemaError)(error) const decoded = yield* Schema.decodeEffect(AiError.UnsupportedSchemaError)(encoded) assert.strictEqual(decoded._tag, "UnsupportedSchemaError") assert.strictEqual(decoded.description, "Unions are not supported") })) it.effect("AiErrorReason union with tool errors roundtrip", () => Effect.gen(function*() { const toolNotFound: AiError.AiErrorReason = new AiError.ToolNotFoundError({ toolName: "Unknown", availableTools: ["A", "B"] }) const encoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(toolNotFound) const decoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(encoded) assert.strictEqual(decoded._tag, "ToolNotFoundError") const paramError: AiError.AiErrorReason = new AiError.ToolParameterValidationError({ toolName: "Test", toolParams: {}, description: "Error" }) const paramEncoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(paramError) const paramDecoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(paramEncoded) assert.strictEqual(paramDecoded._tag, "ToolParameterValidationError") })) }) })