@@ -22,12 +22,14 @@ import {
2222 mapConfigToRequestFields ,
2323 parseStructuredResponse ,
2424 validateStructuredResponse ,
25- StructuredOutputError ,
2625 type ResponseEnvelope ,
2726 type NormalizedStructuredOutputConfig ,
2827} from "./StructuredOutput.js" ;
28+ import { ResilientLLMError , type ResilientLLMErrorCode } from "./ResilientLLMError.js" ;
29+ import type { OperationMetadata } from "./types.js" ;
2930
30- export type { SchemaValidationIssue , StructuredOutputErrorInfo } from "./StructuredOutput.js" ;
31+ export type { OperationMetadata } from "./types.js" ;
32+ export type { SchemaValidationIssue } from "./StructuredOutput.js" ;
3133
3234/**
3335 * Options for the ResilientLLM constructor.
@@ -99,28 +101,6 @@ export interface ToolDefinition {
99101 } ;
100102}
101103
102- /**
103- * Metadata about the operation.
104- * TODO: Include all the fields from the LLM API operation metadata object as well.
105- */
106- export interface OperationMetadata {
107- requestId ?: string | null ;
108- operationId ?: string ;
109- startTime ?: number | null ;
110- /** LLM finish reason semantics (e.g. `stop`, `tool_calls`, `length`). */
111- finishReason ?: string | null ;
112- config ?: Record < string , unknown > ;
113- events ?: unknown [ ] ;
114- timing ?: { totalTimeMs ?: number | null ; rateLimitWaitMs ?: number ; httpRequestMs ?: number | null } ;
115- retries ?: unknown [ ] ;
116- rateLimiting ?: { requestedTokens ?: number ; totalWaitMs ?: number } ;
117- circuitBreaker ?: Record < string , unknown > ;
118- http ?: Record < string , unknown > ;
119- cache ?: Record < string , unknown > ;
120- service ?: { attempted ?: string [ ] ; final ?: string } ;
121- usage ?: { prompt_tokens ?: number | null ; completion_tokens ?: number | null ; total_tokens ?: number | null } ;
122- [ key : string ] : unknown ;
123- }
124104
125105/**
126106 * Always-structured response envelope returned by `ResilientLLM.chat()`.
@@ -286,6 +266,9 @@ class ResilientLLM {
286266 * @param llmOptions - Overrides for this call (model, temperature, tools, apiKey, etc.)
287267 * @param observabilityOptions - Observability/metadata options
288268 * @returns A response envelope: `{ content, toolCalls?, metadata }`
269+ * @throws {ResilientLLMError } User-actionable failures with a stable `code` for branching,
270+ * `metadata` mirroring success metadata, and `cause` for logging. See `error.code` catalog
271+ * in `ResilientLLMError.ts`.
289272 */
290273 async chat (
291274 conversationHistory : ChatMessage [ ] ,
@@ -502,7 +485,10 @@ class ResilientLLM {
502485 llmOptions ?. output_config ?? this . output_config ,
503486 ) ;
504487 if ( ! structuredOutputConfigResult . ok ) {
505- throw new StructuredOutputError ( structuredOutputConfigResult . error ) ;
488+ const soErr = structuredOutputConfigResult . error ;
489+ throw new ResilientLLMError ( soErr . message , soErr . code , {
490+ cause : soErr ,
491+ } ) ;
506492 }
507493 const normalizedStructuredOutputConfig = structuredOutputConfigResult . data ;
508494 const structuredOutputConfig = normalizedStructuredOutputConfig . _source === 'none'
@@ -605,7 +591,7 @@ class ResilientLLM {
605591 /** Parses and normalizes the provider response into caller-facing content.
606592 * @param input - The input object containing the raw data, chat configuration, and tools.
607593 * @returns The parsed chat response.
608- * @throws {StructuredOutputError } If the structured output parsing or validation fails.
594+ * @throws {ResilientLLMError } If the structured output parsing or validation fails.
609595 * @example
610596 * const parsedChatResponse = this._handleResponse({
611597 * rawData: rawProviderResponse,
@@ -645,7 +631,10 @@ class ResilientLLM {
645631 }
646632 }
647633 ) ;
648- throw new StructuredOutputError ( parseResult . error ) ;
634+ const soErr = parseResult . error ;
635+ throw new ResilientLLMError ( soErr . message , soErr . code , {
636+ cause : soErr ,
637+ } ) ;
649638 }
650639
651640 if (
@@ -668,7 +657,10 @@ class ResilientLLM {
668657 validationError : validateResult . error ,
669658 }
670659 ) ;
671- throw new StructuredOutputError ( validateResult . error ) ;
660+ const soErr = validateResult . error ;
661+ throw new ResilientLLMError ( soErr . message , soErr . code , {
662+ cause : soErr ,
663+ } ) ;
672664 }
673665 content = validateResult . data ;
674666 } else {
@@ -902,45 +894,54 @@ class ResilientLLM {
902894 }
903895
904896 parseError ( statusCode : number | null , error : Error , operationMetadata ?: OperationMetadata | null ) : never {
905- let err : Error & { metadata ?: OperationMetadata } ;
897+ if ( error instanceof ResilientLLMError ) {
898+ if ( operationMetadata && ! error . metadata ) {
899+ throw new ResilientLLMError ( error . message , error . code , {
900+ cause : error . cause ,
901+ metadata : operationMetadata ,
902+ retryable : error . retryable ,
903+ } ) ;
904+ }
905+ throw error ;
906+ }
907+ const { message, code } = ResilientLLM . _mapHttpStatus ( statusCode , error ) ;
908+ const metadata : OperationMetadata = {
909+ ...( operationMetadata ?? { } ) ,
910+ provider : { httpStatus : statusCode } ,
911+ } ;
912+ throw new ResilientLLMError ( message , code , {
913+ cause : error ,
914+ metadata,
915+ } ) ;
916+ }
917+
918+ /** Maps an HTTP status to a stable error code and message. */
919+ private static _mapHttpStatus (
920+ statusCode : number | null ,
921+ error : Error ,
922+ ) : { message : string ; code : ResilientLLMErrorCode } {
906923 switch ( statusCode ) {
907924 case 400 :
908925 console . error ( "Bad request" ) ;
909- err = new Error ( error ?. message || "Bad request" ) ;
910- break ;
926+ return { message : error ?. message || "Bad request" , code : "PROVIDER_BAD_REQUEST" } ;
911927 case 401 :
912928 console . error ( "Invalid API Key" ) ;
913- err = new Error ( error ?. message || "Invalid API Key" ) ;
914- break ;
929+ return { message : error ?. message || "Invalid API Key" , code : "PROVIDER_UNAUTHORIZED" } ;
915930 case 403 :
916- err = new Error ( error ?. message || "You are not authorized to access this resource" ) ;
917- break ;
931+ return { message : error ?. message || "You are not authorized to access this resource" , code : "PROVIDER_FORBIDDEN" } ;
918932 case 429 :
919- err = new Error ( error ?. message || "Rate limit exceeded" ) ;
920- break ;
933+ return { message : error ?. message || "Rate limit exceeded" , code : "PROVIDER_RATE_LIMIT" } ;
921934 case 404 :
922- err = new Error ( error ?. message || "Not found" ) ;
923- break ;
935+ return { message : error ?. message || "Not found" , code : "PROVIDER_NOT_FOUND" } ;
924936 case 500 :
925- err = new Error ( error ?. message || "Internal server error" ) ;
926- break ;
937+ return { message : error ?. message || "Internal server error" , code : "PROVIDER_INTERNAL_ERROR" } ;
927938 case 503 :
928- err = new Error ( error ?. message || "Service unavailable" ) ;
929- break ;
939+ return { message : error ?. message || "Service unavailable" , code : "PROVIDER_UNAVAILABLE" } ;
930940 case 529 :
931- err = new Error ( error ?. message || "API temporarily overloaded" ) ;
932- break ;
941+ return { message : error ?. message || "API temporarily overloaded" , code : "PROVIDER_OVERLOADED" } ;
933942 default :
934- err = new Error ( error ?. message || "Unknown error" ) ;
935- }
936- const passthrough = Object . fromEntries (
937- Object . entries ( error as unknown as Record < string , unknown > ) . filter ( ( [ key ] ) => ! [ 'name' , 'message' , 'stack' ] . includes ( key ) )
938- ) ;
939- Object . assign ( err , passthrough ) ;
940- if ( operationMetadata ) {
941- err . metadata = operationMetadata ;
943+ return { message : error ?. message || "Unknown error" , code : "PROVIDER_ERROR" } ;
942944 }
943- throw err ;
944945 }
945946
946947 /**
0 commit comments