Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden Cline 3b9a831c0f fix(ai): require provider error data 2026-08-17 11:31:15 -05:00
Aiden Cline 00707dee46 fix(ai): preserve provider stream error data 2026-08-17 11:02:28 -05:00
23 changed files with 335 additions and 132 deletions
+26 -16
View File
@@ -283,21 +283,27 @@ const AnthropicStreamDelta = Schema.Struct({
stop_sequence: optionalNull(Schema.String), stop_sequence: optionalNull(Schema.String),
}) })
const AnthropicEvent = Schema.Struct({ const AnthropicEvent = Schema.StructWithRest(
type: Schema.String, Schema.Struct({
index: Schema.optional(Schema.Number), type: Schema.String,
message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })), index: Schema.optional(Schema.Number),
content_block: Schema.optional(AnthropicStreamBlock), message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
delta: Schema.optional(AnthropicStreamDelta), content_block: Schema.optional(AnthropicStreamBlock),
usage: Schema.optional(AnthropicUsage), delta: Schema.optional(AnthropicStreamDelta),
// `type` and `message` are both required per Anthropic's spec, but usage: Schema.optional(AnthropicUsage),
// OpenAI-compatible proxies and gateway translations occasionally drop one // `type` and `message` are both required per Anthropic's spec, but
// or the other; mark them optional so a partial payload still parses and // OpenAI-compatible proxies and gateway translations occasionally drop one
// the parser can fall back to whichever field is populated. // or the other; mark them optional so a partial payload still parses and
error: Schema.optional( // the parser can fall back to whichever field is populated.
Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }), error: Schema.optional(
), Schema.StructWithRest(
}) Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent> type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
interface ParserState { interface ParserState {
@@ -983,7 +989,11 @@ const onError = (event: AnthropicEvent) =>
new AIError({ new AIError({
module: ADAPTER, module: ADAPTER,
method: "stream", method: "stream",
reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }), reason: classifyProviderFailure({
message: providerErrorMessage(event),
code: event.error?.type,
data: Schema.decodeUnknownSync(Schema.Json)(event),
}),
}) })
const step = (state: ParserState, event: AnthropicEvent) => { const step = (state: ParserState, event: AnthropicEvent) => {
+65 -58
View File
@@ -155,69 +155,75 @@ const BedrockUsageSchema = Schema.Struct({
}) })
type BedrockUsageSchema = Schema.Schema.Type<typeof BedrockUsageSchema> type BedrockUsageSchema = Schema.Schema.Type<typeof BedrockUsageSchema>
const BedrockStreamException = Schema.Struct({ const BedrockStreamException = Schema.StructWithRest(
message: Schema.optional(Schema.String), Schema.Struct({
originalMessage: Schema.optional(Schema.String), message: Schema.optional(Schema.String),
originalStatusCode: Schema.optional(Schema.Number), originalMessage: Schema.optional(Schema.String),
}) originalStatusCode: Schema.optional(Schema.Number),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
// Streaming event shape — the AWS event stream wraps each JSON payload by its // Streaming event shape — the AWS event stream wraps each JSON payload by its
// `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We // `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We
// reconstruct that wrapping in `decodeFrames` below so the event schema can // reconstruct that wrapping in `decodeFrames` below so the event schema can
// stay a plain discriminated record. // stay a plain discriminated record.
const BedrockEvent = Schema.Struct({ const BedrockEvent = Schema.StructWithRest(
messageStart: Schema.optional(Schema.Struct({ role: Schema.String })), Schema.Struct({
contentBlockStart: Schema.optional( messageStart: Schema.optional(Schema.Struct({ role: Schema.String })),
Schema.Struct({ contentBlockStart: Schema.optional(
contentBlockIndex: Schema.Number, Schema.Struct({
start: Schema.optional( contentBlockIndex: Schema.Number,
Schema.Struct({ start: Schema.optional(
toolUse: Schema.optional(Schema.Struct({ toolUseId: Schema.String, name: Schema.String })), Schema.Struct({
}), toolUse: Schema.optional(Schema.Struct({ toolUseId: Schema.String, name: Schema.String })),
), }),
}), ),
), }),
contentBlockDelta: Schema.optional( ),
Schema.Struct({ contentBlockDelta: Schema.optional(
contentBlockIndex: Schema.Number, Schema.Struct({
delta: Schema.optional( contentBlockIndex: Schema.Number,
Schema.Struct({ delta: Schema.optional(
text: Schema.optional(Schema.String), Schema.Struct({
toolUse: Schema.optional(Schema.Struct({ input: Schema.String })), text: Schema.optional(Schema.String),
reasoningContent: Schema.optional( toolUse: Schema.optional(Schema.Struct({ input: Schema.String })),
Schema.Struct({ reasoningContent: Schema.optional(
text: Schema.optional(Schema.String), Schema.Struct({
signature: Schema.optional(Schema.String), text: Schema.optional(Schema.String),
// Blob fields in Bedrock's JSON event stream are base64 strings. signature: Schema.optional(Schema.String),
redactedContent: Schema.optional(Schema.String), // Blob fields in Bedrock's JSON event stream are base64 strings.
// Vercel's Bedrock provider exposes the same delta under redactedContent: Schema.optional(Schema.String),
// Anthropic's shorter `data` spelling. // Vercel's Bedrock provider exposes the same delta under
data: Schema.optional(Schema.String), // Anthropic's shorter `data` spelling.
}), data: Schema.optional(Schema.String),
), }),
}), ),
), }),
}), ),
), }),
contentBlockStop: Schema.optional(Schema.Struct({ contentBlockIndex: Schema.Number })), ),
messageStop: Schema.optional( contentBlockStop: Schema.optional(Schema.Struct({ contentBlockIndex: Schema.Number })),
Schema.Struct({ messageStop: Schema.optional(
stopReason: Schema.String, Schema.Struct({
additionalModelResponseFields: Schema.optional(Schema.Unknown), stopReason: Schema.String,
}), additionalModelResponseFields: Schema.optional(Schema.Unknown),
), }),
metadata: Schema.optional( ),
Schema.Struct({ metadata: Schema.optional(
usage: Schema.optional(BedrockUsageSchema), Schema.Struct({
metrics: Schema.optional(Schema.Unknown), usage: Schema.optional(BedrockUsageSchema),
}), metrics: Schema.optional(Schema.Unknown),
), }),
internalServerException: Schema.optional(BedrockStreamException), ),
modelStreamErrorException: Schema.optional(BedrockStreamException), internalServerException: Schema.optional(BedrockStreamException),
validationException: Schema.optional(BedrockStreamException), modelStreamErrorException: Schema.optional(BedrockStreamException),
throttlingException: Schema.optional(BedrockStreamException), validationException: Schema.optional(BedrockStreamException),
serviceUnavailableException: Schema.optional(BedrockStreamException), throttlingException: Schema.optional(BedrockStreamException),
}) serviceUnavailableException: Schema.optional(BedrockStreamException),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent> type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent>
// ============================================================================= // =============================================================================
@@ -666,6 +672,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
reason: classifyProviderFailure({ reason: classifyProviderFailure({
message: exception[1]?.message ?? exception[1]?.originalMessage ?? "Bedrock Converse stream error", message: exception[1]?.message ?? exception[1]?.originalMessage ?? "Bedrock Converse stream error",
code: exception[0], code: exception[0],
data: Schema.decodeUnknownSync(Schema.Json)(event),
}), }),
}) })
} }
+6 -1
View File
@@ -1012,7 +1012,12 @@ export const providerFailure = (id: string, event: Event, fallback: string) => {
return new AIError({ return new AIError({
module: id, module: id,
method: "stream", method: "stream",
reason: classifyProviderFailure({ message, code, status }), reason: classifyProviderFailure({
message,
code,
status,
data: Schema.decodeUnknownSync(Schema.Json)(event),
}),
}) })
} }
+16 -9
View File
@@ -209,16 +209,22 @@ const OpenAIChatChoice = Schema.Struct({
native_finish_reason: optionalNull(Schema.String), native_finish_reason: optionalNull(Schema.String),
}) })
const OpenAIChatError = Schema.Struct({ const OpenAIChatError = Schema.StructWithRest(
code: optionalNull(Schema.Union([Schema.String, Schema.Number])), Schema.Struct({
message: Schema.String, code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
}) message: Schema.String,
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
export const OpenAIChatEvent = Schema.Struct({ export const OpenAIChatEvent = Schema.StructWithRest(
choices: optionalNull(Schema.Array(OpenAIChatChoice)), Schema.Struct({
usage: optionalNull(OpenAIChatUsage), choices: optionalNull(Schema.Array(OpenAIChatChoice)),
error: optionalNull(OpenAIChatError), usage: optionalNull(OpenAIChatUsage),
}) error: optionalNull(OpenAIChatError),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent> export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>
type OpenAIChatRequestMessage = LLMRequest["messages"][number] type OpenAIChatRequestMessage = LLMRequest["messages"][number]
@@ -687,6 +693,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
message: event.error.message, message: event.error.message,
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code), code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
status: typeof event.error.code === "number" ? event.error.code : undefined, status: typeof event.error.code === "number" ? event.error.code : undefined,
data: Schema.decodeUnknownSync(Schema.Json)(event),
}), }),
}) })
const events: LLMEvent[] = [] const events: LLMEvent[] = []
+7 -1
View File
@@ -77,6 +77,7 @@ const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i
export interface ProviderFailure { export interface ProviderFailure {
readonly message: string readonly message: string
readonly data: typeof Schema.Json.Type
readonly status?: number | undefined readonly status?: number | undefined
readonly code?: string | undefined readonly code?: string | undefined
readonly retryAfterMs?: number | undefined readonly retryAfterMs?: number | undefined
@@ -93,7 +94,12 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason
.filter((code): code is string => code !== undefined) .filter((code): code is string => code !== undefined)
.map((code) => code.toLowerCase()) .map((code) => code.toLowerCase())
const text = body || input.message const text = body || input.message
const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http } const common = {
message: input.message,
data: input.data,
providerMetadata: input.providerMetadata,
http: input.http,
}
const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500) const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500)
if ( if (
+3
View File
@@ -173,6 +173,7 @@ const statusError =
reason: classifyProviderFailure({ reason: classifyProviderFailure({
status: response.status, status: response.status,
message: providerMessage(response.status, body), message: providerMessage(response.status, body),
data: body ?? null,
retryAfterMs: retryAfter, retryAfterMs: retryAfter,
rateLimit, rateLimit,
http: responseHttp({ http: responseHttp({
@@ -193,6 +194,7 @@ const statusError =
// request headers are empty. // request headers are empty.
export const classifyHttpFailure = (input: { export const classifyHttpFailure = (input: {
readonly message: string readonly message: string
readonly data: typeof Schema.Json.Type
readonly url: string readonly url: string
readonly status?: number | undefined readonly status?: number | undefined
readonly code?: string | undefined readonly code?: string | undefined
@@ -205,6 +207,7 @@ export const classifyHttpFailure = (input: {
const details = responseBody(input.responseBody) const details = responseBody(input.responseBody)
return classifyProviderFailure({ return classifyProviderFailure({
message: input.message, message: input.message,
data: input.data,
status: input.status, status: input.status,
code: input.code, code: input.code,
retryAfterMs: retryAfter, retryAfterMs: retryAfter,
+7
View File
@@ -35,6 +35,7 @@ export class HttpContext extends Schema.Class<HttpContext>("AI.HttpContext")({
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({ export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
_tag: Schema.tag("InvalidRequest"), _tag: Schema.tag("InvalidRequest"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
parameter: Schema.optional(Schema.String), parameter: Schema.optional(Schema.String),
classification: Schema.optional(ProviderFailureClassification), classification: Schema.optional(ProviderFailureClassification),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
@@ -55,6 +56,7 @@ export class NoRouteReason extends Schema.Class<NoRouteReason>("AI.Error.NoRoute
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
_tag: Schema.tag("Authentication"), _tag: Schema.tag("Authentication"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]), kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
http: Schema.optional(HttpContext), http: Schema.optional(HttpContext),
@@ -63,6 +65,7 @@ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI
export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({ export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
_tag: Schema.tag("RateLimit"), _tag: Schema.tag("RateLimit"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
retryAfterMs: Schema.optional(Schema.Number), retryAfterMs: Schema.optional(Schema.Number),
rateLimit: Schema.optional(HttpRateLimitDetails), rateLimit: Schema.optional(HttpRateLimitDetails),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
@@ -72,6 +75,7 @@ export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.Rat
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({ export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
_tag: Schema.tag("QuotaExceeded"), _tag: Schema.tag("QuotaExceeded"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
http: Schema.optional(HttpContext), http: Schema.optional(HttpContext),
}) {} }) {}
@@ -79,6 +83,7 @@ export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.E
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({ export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
_tag: Schema.tag("ContentPolicy"), _tag: Schema.tag("ContentPolicy"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
http: Schema.optional(HttpContext), http: Schema.optional(HttpContext),
}) {} }) {}
@@ -86,6 +91,7 @@ export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.E
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({ export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
_tag: Schema.tag("ProviderInternal"), _tag: Schema.tag("ProviderInternal"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
status: Schema.optional(Schema.Number), status: Schema.optional(Schema.Number),
retryAfterMs: Schema.optional(Schema.Number), retryAfterMs: Schema.optional(Schema.Number),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
@@ -129,6 +135,7 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({ export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
_tag: Schema.tag("UnknownProvider"), _tag: Schema.tag("UnknownProvider"),
message: Schema.String, message: Schema.String,
data: Schema.optional(Schema.Json),
status: Schema.optional(Schema.Number), status: Schema.optional(Schema.Number),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
http: Schema.optional(HttpContext), http: Schema.optional(HttpContext),
+1
View File
@@ -216,6 +216,7 @@ export type Finish = Schema.Schema.Type<typeof Finish>
export const ProviderErrorEvent = Schema.Struct({ export const ProviderErrorEvent = Schema.Struct({
type: Schema.tag("provider-error"), type: Schema.tag("provider-error"),
message: Schema.String, message: Schema.String,
data: Schema.Json,
classification: Schema.optional(ProviderFailureClassification), classification: Schema.optional(ProviderFailureClassification),
providerMetadata: Schema.optional(ProviderMetadata), providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ProviderError" }) }).annotate({ identifier: "LLM.Event.ProviderError" })
@@ -1031,12 +1031,24 @@ describe("Anthropic Messages route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe( const error = yield* LLMClient.generate(request).pipe(
Effect.provide( Effect.provide(
fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "Overloaded" } })), fixedResponse(
sseEvents({
type: "error",
error: { type: "overloaded_error", message: "Overloaded", request_id: "req_123" },
}),
),
), ),
Effect.flip, Effect.flip,
) )
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "overloaded_error: Overloaded" }) expect(error.reason).toMatchObject({
_tag: "ProviderInternal",
message: "overloaded_error: Overloaded",
data: {
type: "error",
error: { type: "overloaded_error", message: "Overloaded", request_id: "req_123" },
},
})
}), }),
) )
@@ -714,11 +714,15 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const body = concat([ const body = concat([
eventFrame("messageStart", { role: "assistant" }), eventFrame("messageStart", { role: "assistant" }),
exceptionFrame("throttlingException", { message: "Slow down" }), exceptionFrame("throttlingException", { message: "Slow down", requestId: "req_123" }),
]) ])
const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip) const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip)
expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "Slow down" }) expect(error.reason).toMatchObject({
_tag: "RateLimit",
message: "Slow down",
data: { throttlingException: { message: "Slow down", requestId: "req_123" } },
})
}), }),
) )
@@ -2592,13 +2592,25 @@ describe("OpenAI Responses route", () => {
message: "Something went wrong", message: "Something went wrong",
param: null, param: null,
sequence_number: 1, sequence_number: 1,
diagnostic: { region: "us-east" },
}), }),
), ),
), ),
Effect.flip, Effect.flip,
) )
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Something went wrong" }) expect(error.reason).toMatchObject({
_tag: "UnknownProvider",
message: "Something went wrong",
data: {
type: "error",
code: null,
message: "Something went wrong",
param: null,
sequence_number: 1,
diagnostic: { region: "us-east" },
},
})
}), }),
) )
+5 -2
View File
@@ -253,14 +253,17 @@ describe("OpenRouter", () => {
Effect.provide( Effect.provide(
fixedResponse( fixedResponse(
sseEvents({ sseEvents({
error: { code: 502, message: "Provider disconnected" }, error: { code: 502, message: "Provider disconnected", upstream: "openai" },
}), }),
), ),
), ),
Effect.flip, Effect.flip,
) )
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" }) expect(error.reason).toMatchObject({
_tag: "ProviderInternal",
data: { error: { code: 502, message: "Provider disconnected", upstream: "openai" } },
})
expect(error.message).toContain("Provider disconnected") expect(error.message).toContain("Provider disconnected")
}), }),
) )
+30 -5
View File
@@ -478,7 +478,12 @@ export type Endpoint5_31Output =
readonly location?: Location.Ref | undefined readonly location?: Location.Ref | undefined
readonly data: { readonly data: {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined } readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly data?: Schema.Json | undefined
}
} }
} }
| { | {
@@ -605,7 +610,12 @@ export type Endpoint5_31Output =
readonly data: { readonly data: {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly assistantMessageID: SessionMessage.ID readonly assistantMessageID: SessionMessage.ID
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined } readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly data?: Schema.Json | undefined
}
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
readonly tokens?: readonly tokens?:
| { | {
@@ -767,7 +777,12 @@ export type Endpoint5_31Output =
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly assistantMessageID: SessionMessage.ID readonly assistantMessageID: SessionMessage.ID
readonly id: string readonly id: string
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined } readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly data?: Schema.Json | undefined
}
readonly content?: readonly content?:
| readonly [ | readonly [
( (
@@ -807,7 +822,12 @@ export type Endpoint5_31Output =
readonly assistantMessageID: SessionMessage.ID readonly assistantMessageID: SessionMessage.ID
readonly attempt: number readonly attempt: number
readonly at: number readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined } readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly data?: Schema.Json | undefined
}
} }
} }
| { | {
@@ -848,7 +868,12 @@ export type Endpoint5_31Output =
readonly data: { readonly data: {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly reason: "auto" | "manual" readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined } readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly data?: Schema.Json | undefined
}
readonly inputID?: SessionMessage.ID | undefined readonly inputID?: SessionMessage.ID | undefined
} }
} }
+73 -13
View File
@@ -104,7 +104,7 @@ export type ToolTextContent = { type: "text"; text: string }
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null } export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
export type SessionStructuredError = { type: string; message: string; status?: number } export type SessionStructuredError = { type: string; message: string; status?: number; data?: JsonValue }
export type SessionMessageCompactionRunning = { export type SessionMessageCompactionRunning = {
type: "compaction" type: "compaction"
@@ -2647,7 +2647,12 @@ export type SessionImportInput = {
| { | {
readonly status: "error" readonly status: "error"
readonly input: { readonly [x: string]: JsonValue } readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly content?: readonly [ readonly content?: readonly [
( (
| { readonly type: "text"; readonly text: string } | { readonly type: "text"; readonly text: string }
@@ -2682,11 +2687,21 @@ export type SessionImportInput = {
readonly reasoning: number readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number } readonly cache: { readonly read: number; readonly write: number }
} }
readonly error?: { readonly type: string; readonly message: string; readonly status?: number } readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly retry?: { readonly retry?: {
readonly attempt: number readonly attempt: number
readonly at: number readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
} }
| ( | (
@@ -2717,7 +2732,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly status: "failed" readonly status: "failed"
readonly reason: "auto" | "manual" readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
) )
> >
@@ -2914,7 +2934,12 @@ export type SessionImportInput = {
| { | {
readonly status: "error" readonly status: "error"
readonly input: { readonly [x: string]: JsonValue } readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly content?: readonly [ readonly content?: readonly [
( (
| { readonly type: "text"; readonly text: string } | { readonly type: "text"; readonly text: string }
@@ -2949,11 +2974,21 @@ export type SessionImportInput = {
readonly reasoning: number readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number } readonly cache: { readonly read: number; readonly write: number }
} }
readonly error?: { readonly type: string; readonly message: string; readonly status?: number } readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly retry?: { readonly retry?: {
readonly attempt: number readonly attempt: number
readonly at: number readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
} }
| ( | (
@@ -2984,7 +3019,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly status: "failed" readonly status: "failed"
readonly reason: "auto" | "manual" readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
) )
> >
@@ -3181,7 +3221,12 @@ export type SessionImportInput = {
| { | {
readonly status: "error" readonly status: "error"
readonly input: { readonly [x: string]: JsonValue } readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly content?: readonly [ readonly content?: readonly [
( (
| { readonly type: "text"; readonly text: string } | { readonly type: "text"; readonly text: string }
@@ -3216,11 +3261,21 @@ export type SessionImportInput = {
readonly reasoning: number readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number } readonly cache: { readonly read: number; readonly write: number }
} }
readonly error?: { readonly type: string; readonly message: string; readonly status?: number } readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
readonly retry?: { readonly retry?: {
readonly attempt: number readonly attempt: number
readonly at: number readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
} }
| ( | (
@@ -3251,7 +3306,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number } readonly time: { readonly created: number }
readonly status: "failed" readonly status: "failed"
readonly reason: "auto" | "manual" readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number } readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly data?: JsonValue
}
} }
) )
> >
+5 -1
View File
@@ -780,7 +780,10 @@ function llmError(method: string, error: unknown) {
? new InvalidProviderOutputReason({ message: error.message }) ? new InvalidProviderOutputReason({ message: error.message })
: APICallError.isInstance(error) : APICallError.isInstance(error)
? apiCallErrorReason(error) ? apiCallErrorReason(error)
: new UnknownProviderReason({ message: unknownErrorMessage(error) }) : new UnknownProviderReason({
message: unknownErrorMessage(error),
data: Schema.decodeUnknownSync(Schema.Json)(jsonValue(error)),
})
return new AIError({ return new AIError({
module: "AISDK", module: "AISDK",
method, method,
@@ -792,6 +795,7 @@ function apiCallErrorReason(error: APICallError) {
const details = providerErrorDetails(error) const details = providerErrorDetails(error)
const reason = RequestExecutor.classifyHttpFailure({ const reason = RequestExecutor.classifyHttpFailure({
message: details.message, message: details.message,
data: Schema.decodeUnknownSync(Schema.Json)(jsonValue(error.data ?? error.responseBody ?? null)),
url: error.url, url: error.url,
status: error.statusCode, status: error.statusCode,
code: details.code, code: details.code,
@@ -528,7 +528,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
return return
case "provider-error": case "provider-error":
providerFailed = true providerFailed = true
yield* failAssistant({ type: "provider.unknown", message: event.message }) yield* failAssistant({ type: "provider.unknown", message: event.message, data: event.data })
return return
} }
}) })
@@ -61,5 +61,11 @@ export function toSessionError(cause: unknown): SessionError.Error {
function providerError(type: string, reason: AIError["reason"]): SessionError.Error { function providerError(type: string, reason: AIError["reason"]): SessionError.Error {
const status = const status =
("http" in reason ? reason.http?.response?.status : undefined) ?? ("status" in reason ? reason.status : undefined) ("http" in reason ? reason.http?.response?.status : undefined) ?? ("status" in reason ? reason.status : undefined)
return { type, message: reason.message, ...(status === undefined ? {} : { status }) } const data = "data" in reason ? reason.data : reason._tag === "InvalidProviderOutput" ? reason.raw : undefined
return {
type,
message: reason.message,
...(status === undefined ? {} : { status }),
...(data === undefined ? {} : { data }),
}
} }
+2
View File
@@ -487,6 +487,7 @@ it.effect("derives status and code when the AI SDK error message is empty", () =
expect(projected.type).toBe("provider.invalid-request") expect(projected.type).toBe("provider.invalid-request")
expect(projected.status).toBe(404) expect(projected.status).toBe(404)
expect(projected.message).not.toBe("") expect(projected.message).not.toBe("")
expect(projected.data).toEqual({ error: { message: "", code: "not_found" } })
}), }),
) )
@@ -504,6 +505,7 @@ it.effect("preserves complete HTTP context on AI SDK call errors", () =>
expect(http?.response?.status).toBe(404) expect(http?.response?.status).toBe(404)
expect(http?.response?.headers["authorization"]).toBe("Bearer secret-token") expect(http?.response?.headers["authorization"]).toBe("Bearer secret-token")
expect(http?.body).toBe('{"error":{"message":"","code":"not_found"}}') expect(http?.body).toBe('{"error":{"message":"","code":"not_found"}}')
expect(error.reason).toMatchObject({ data: '{"error":{"message":"","code":"not_found"}}' })
}), }),
) )
+18
View File
@@ -61,6 +61,24 @@ describe("toSessionError", () => {
).type, ).type,
).toBe("provider.no-route") ).toBe("provider.no-route")
expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown") expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "malformed", raw: "not-json" })))).toEqual({
type: "provider.invalid-output",
message: "malformed",
data: "not-json",
})
})
test("preserves provider error data", () => {
const data = {
type: "error",
sequence_number: 2,
error: { type: "server_error", code: null, message: null },
}
expect(toSessionError(llm(new UnknownProviderReason({ message: "stream error", data })))).toEqual({
type: "provider.unknown",
message: "stream error",
data,
})
}) })
test("preserves the permission rejection type without exposing internal fields", () => { test("preserves the permission rejection type without exposing internal fields", () => {
+23 -18
View File
@@ -925,7 +925,7 @@ describe("SessionRunnerLLM", () => {
yield* admit(session, "Second prompt") yield* admit(session, "Second prompt")
const titleFailed = yield* Deferred.make<void>() const titleFailed = yield* Deferred.make<void>()
yield* TestLLM.push( yield* TestLLM.push(
Stream.make(LLMEvent.providerError({ message: "Title provider unavailable" })).pipe( Stream.make(LLMEvent.providerError({ message: "Title provider unavailable", data: {} })).pipe(
Stream.ensuring(Deferred.succeed(titleFailed, undefined)), Stream.ensuring(Deferred.succeed(titleFailed, undefined)),
), ),
TestLLM.text("Recovered", "text-recovered"), TestLLM.text("Recovered", "text-recovered"),
@@ -2179,7 +2179,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push(TestLLM.text("Earlier answer", "text-manual-provider-history")) yield* TestLLM.push(TestLLM.text("Earlier answer", "text-manual-provider-history"))
yield* runPrompt(session, "Earlier question") yield* runPrompt(session, "Earlier question")
yield* TestLLM.push([LLMEvent.providerError({ message: "summary unavailable" })]) yield* TestLLM.push([LLMEvent.providerError({ message: "summary unavailable", data: {} })])
const compaction = yield* session.compact({ sessionID }) const compaction = yield* session.compact({ sessionID })
yield* session.resume(sessionID) yield* session.resume(sessionID)
@@ -2337,7 +2337,7 @@ describe("SessionRunnerLLM", () => {
currentModel = compactModel currentModel = compactModel
requests.length = 0 requests.length = 0
yield* TestLLM.push( yield* TestLLM.push(
[LLMEvent.providerError({ message: "Unsupported parameter: max_output_tokens" })], [LLMEvent.providerError({ message: "Unsupported parameter: max_output_tokens", data: {} })],
TestLLM.text("Must not run", "text-after-failed-compaction"), TestLLM.text("Must not run", "text-after-failed-compaction"),
) )
yield* admit(session, "Recent exact request ".repeat(180)) yield* admit(session, "Recent exact request ".repeat(180))
@@ -2362,7 +2362,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push( yield* TestLLM.push(
[ [
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }), LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
], ],
TestLLM.text("## Objective\n- Recover overflow", "text-summary"), TestLLM.text("## Objective\n- Recover overflow", "text-summary"),
TestLLM.text("Recovered", "text-final"), TestLLM.text("Recovered", "text-final"),
@@ -2389,7 +2389,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* setupOverflowRecovery const session = yield* setupOverflowRecovery
currentModel = model currentModel = model
yield* TestLLM.push( yield* TestLLM.push(
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })], [LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
TestLLM.text("## Objective\n- Recover unknown limit", "text-summary-unknown-limit"), TestLLM.text("## Objective\n- Recover unknown limit", "text-summary-unknown-limit"),
TestLLM.text("Recovered", "text-final-unknown-limit"), TestLLM.text("Recovered", "text-final-unknown-limit"),
) )
@@ -2408,7 +2408,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* setupOverflowRecovery const session = yield* setupOverflowRecovery
currentModel = undersizedContextModel currentModel = undersizedContextModel
yield* TestLLM.push( yield* TestLLM.push(
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })], [LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
TestLLM.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"), TestLLM.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
TestLLM.text("Recovered", "text-final-undersized-limit"), TestLLM.text("Recovered", "text-final-undersized-limit"),
) )
@@ -2427,7 +2427,7 @@ describe("SessionRunnerLLM", () => {
const session = yield* setupOverflowRecovery const session = yield* setupOverflowRecovery
const overflow = () => [ const overflow = () => [
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }), LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
] ]
yield* TestLLM.push(overflow(), TestLLM.text("## Objective\n- Recover once", "text-summary"), overflow()) yield* TestLLM.push(overflow(), TestLLM.text("## Objective\n- Recover once", "text-summary"), overflow())
yield* admit(session, "Continue") yield* admit(session, "Continue")
@@ -2474,8 +2474,8 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setupOverflowRecovery const session = yield* setupOverflowRecovery
yield* TestLLM.push( yield* TestLLM.push(
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })], [LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
[LLMEvent.providerError({ message: "summary unavailable" })], [LLMEvent.providerError({ message: "summary unavailable", data: {} })],
) )
yield* admit(session, "Continue") yield* admit(session, "Continue")
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long") expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
@@ -2502,7 +2502,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setupOverflowRecovery const session = yield* setupOverflowRecovery
yield* TestLLM.push( yield* TestLLM.push(
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })], [LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
TestLLM.text("## Objective\n- Interrupted", "text-summary"), TestLLM.text("## Objective\n- Interrupted", "text-summary"),
) )
const first = yield* TestLLM.gate const first = yield* TestLLM.gate
@@ -4078,9 +4078,10 @@ describe("SessionRunnerLLM", () => {
it.effect("projects provider errors as terminal assistant step failures", () => it.effect("projects provider errors as terminal assistant step failures", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setup const session = yield* setup
const data = { type: "error", error: { type: "server_error" } }
yield* TestLLM.push([ yield* TestLLM.push([
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.providerError({ message: "Provider unavailable" }), LLMEvent.providerError({ message: "Provider unavailable", data }),
]) ])
expect((yield* runPrompt(session, "Fail durably").pipe(Effect.flip)).message).toBe("Provider unavailable") expect((yield* runPrompt(session, "Fail durably").pipe(Effect.flip)).message).toBe("Provider unavailable")
@@ -4088,7 +4089,11 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1) expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([ expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Fail durably" }, { type: "user", text: "Fail durably" },
{ type: "assistant", finish: "error", error: { type: "provider.unknown", message: "Provider unavailable" } }, {
type: "assistant",
finish: "error",
error: { type: "provider.unknown", message: "Provider unavailable", data },
},
]) ])
}), }),
) )
@@ -4096,7 +4101,7 @@ describe("SessionRunnerLLM", () => {
it.effect("projects provider errors emitted before assistant step start", () => it.effect("projects provider errors emitted before assistant step start", () =>
Effect.gen(function* () { Effect.gen(function* () {
const session = yield* setup const session = yield* setup
yield* TestLLM.push([LLMEvent.providerError({ message: "Provider unavailable" })]) yield* TestLLM.push([LLMEvent.providerError({ message: "Provider unavailable", data: {} })])
expect((yield* runPrompt(session, "Fail before step").pipe(Effect.flip)).message).toBe("Provider unavailable") expect((yield* runPrompt(session, "Fail before step").pipe(Effect.flip)).message).toBe("Provider unavailable")
@@ -4183,7 +4188,7 @@ describe("SessionRunnerLLM", () => {
LLMEvent.textStart({ id: "text-partial" }), LLMEvent.textStart({ id: "text-partial" }),
LLMEvent.textDelta({ id: "text-partial", text: "Partial" }), LLMEvent.textDelta({ id: "text-partial", text: "Partial" }),
LLMEvent.textEnd({ id: "text-partial" }), LLMEvent.textEnd({ id: "text-partial" }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }), LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
]) ])
expect((yield* runPrompt(session, "Fail after output").pipe(Effect.flip)).message).toBe("prompt too long") expect((yield* runPrompt(session, "Fail after output").pipe(Effect.flip)).message).toBe("prompt too long")
@@ -4958,7 +4963,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push([ yield* TestLLM.push([
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }), LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }),
LLMEvent.providerError({ message: "Provider unavailable" }), LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
]) ])
const run = yield* session.resume(sessionID).pipe(Effect.forkChild) const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
@@ -4985,7 +4990,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push([ yield* TestLLM.push([
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
hostedCall("call-hosted-provider-error", "effect"), hostedCall("call-hosted-provider-error", "effect"),
LLMEvent.providerError({ message: "Provider unavailable" }), LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
]) ])
expect((yield* runPrompt(session, "Fail hosted tool durably").pipe(Effect.flip)).message).toBe( expect((yield* runPrompt(session, "Fail hosted tool durably").pipe(Effect.flip)).message).toBe(
@@ -5017,7 +5022,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push([ yield* TestLLM.push([
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }), LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
LLMEvent.providerError({ message: "Provider unavailable" }), LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
]) ])
expect((yield* runPrompt(session, "Defect while provider fails").pipe(Effect.flip)).message).toBe( expect((yield* runPrompt(session, "Defect while provider fails").pipe(Effect.flip)).message).toBe(
@@ -5044,7 +5049,7 @@ describe("SessionRunnerLLM", () => {
yield* TestLLM.push([ yield* TestLLM.push([
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-store-provider-error", name: "storefail", input: {} }), LLMEvent.toolCall({ id: "call-store-provider-error", name: "storefail", input: {} }),
LLMEvent.providerError({ message: "Provider unavailable" }), LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
]) ])
expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({ expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({
+1 -1
View File
@@ -310,7 +310,7 @@ it.effect("retries after a failed title request", () =>
yield* insertSession(sessionID) yield* insertSession(sessionID)
yield* prompt(sessionID, "Retry this title") yield* prompt(sessionID, "Retry this title")
const title = yield* SessionTitle.Service const title = yield* SessionTitle.Service
titleStream = () => Stream.make(LLMEvent.providerError({ message: "Provider unavailable" })) titleStream = () => Stream.make(LLMEvent.providerError({ message: "Provider unavailable", data: {} }))
yield* title.generateForFirstPrompt(sessionID) yield* title.generateForFirstPrompt(sessionID)
titleStream = successfulTitle titleStream = successfulTitle
+1
View File
@@ -8,4 +8,5 @@ export const Error = Schema.Struct({
type: Schema.String, type: Schema.String,
message: Schema.String, message: Schema.String,
status: Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 })).pipe(optional), status: Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 })).pipe(optional),
data: Schema.Json.pipe(optional),
}).annotate({ identifier: "Session.StructuredError" }) }).annotate({ identifier: "Session.StructuredError" })
@@ -12,6 +12,11 @@ describe("SessionError", () => {
const values: SessionError.Error[] = [ const values: SessionError.Error[] = [
{ type: "provider.rate-limit", message: "Slow down" }, { type: "provider.rate-limit", message: "Slow down" },
{ type: "provider.auth", message: "Authentication failed" }, { type: "provider.auth", message: "Authentication failed" },
{
type: "provider.unknown",
message: "Stream error",
data: { type: "error", error: { type: "server_error" } },
},
{ type: "provider.future-condition", message: "A future provider failure" }, { type: "provider.future-condition", message: "A future provider failure" },
{ type: "unknown", message: "Unexpected" }, { type: "unknown", message: "Unexpected" },
] ]