Skip to content

Commit b67c19e

Browse files
committed
refactor: error handling
1 parent 5e1c22b commit b67c19e

9 files changed

Lines changed: 287 additions & 108 deletions

CHANGELOG.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,37 @@
22

33
## [1.7.2](https://github.com/gitcommitshow/resilient-llm/compare/v1.7.1...v1.7.2) (2026-03-20)
44

5-
65
### Bug Fixes
76

87
* refactor and add structured output support ([#85](https://github.com/gitcommitshow/resilient-llm/issues/85)) ([c79b9ee](https://github.com/gitcommitshow/resilient-llm/commit/c79b9ee169f5e8909faca3010aabbf5470f982ec))
98

10-
## [Unreleased]
11-
129
### Breaking Changes
1310
* `ResilientLLM.chat()` now always returns a consistent envelope object: `{ content, toolCalls?, metadata }` (metadata is no longer gated by `returnOperationMetadata`).
1411

12+
This is how you use the library after v1.7.2
13+
14+
```javascript
15+
import { ResilientLLM } from 'resilient-llm';
16+
17+
const llm = new ResilientLLM({
18+
aiService: 'openai',
19+
model: 'gpt-4o-mini',
20+
});
21+
22+
const conversationHistory = [
23+
{ role: 'system', content: 'You are a helpful assistant.' },
24+
{ role: 'assistant', content: 'Hi, I am here to help.' },
25+
{ role: 'user', content: 'What is the capital of France?' }
26+
];
27+
28+
try {
29+
const { content, toolCalls, metadata } = await llm.chat(conversationHistory);
30+
console.log('LLM response:', content);
31+
} catch (err) {
32+
console.error('Error:', err);
33+
}
34+
```
35+
1536
## [1.7.1](https://github.com/gitcommitshow/resilient-llm/compare/v1.7.0...v1.7.1) (2026-03-16)
1637

1738

README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,50 @@ See the [full API reference](./docs/reference.md) for complete documentation.
9696

9797
Use `llm.chat(..., { responseFormat })` when you need the assistant to return **machine-readable JSON**, optionally matching a **specific JSON Schema**.
9898

99+
### Simple JSON without any specific schema
100+
99101
```javascript
100102
// JSON mode (single JSON object)
101103
const { content: obj } = await llm.chat(messages, { responseFormat: { type: 'json_object' } });
104+
```
105+
106+
## JSON with a specific schema only
102107

103-
// Schema mode (validate required keys/types)
104-
const { content: result } = await llm.chat(messages, {
108+
```javascript
109+
const conversationHistory = [{ role: 'user', content: 'Add 2 and 3 and respond ONLY with JSON having sum and explanationSteps' }];
110+
const { content: result } = await llm.chat(conversationHistory, {
105111
responseFormat: {
106112
type: 'json_schema',
107113
json_schema: {
108-
name: 'answer_payload',
114+
name: 'math_answer',
109115
schema: {
110116
type: 'object',
111-
additionalProperties: false,
112-
properties: { answer: { type: 'string' } },
113-
required: ['answer']
117+
properties: {
118+
sum: { type: 'number' },
119+
explanationSteps: {
120+
type: 'array',
121+
items: { type: 'string' }
122+
}
123+
},
124+
required: ['sum', 'explanationSteps'],
125+
// Anthropic Messages structured output requires explicit false here for object roots.
126+
additionalProperties: false
114127
}
115-
}
116-
}
128+
}
117129
});
118130

119131
// `result` is a parsed JS object (not a string).
132+
console.log(JSON.stringify(result))
133+
// {
134+
// content: {
135+
// sum: 5,
136+
// explanationSteps: [
137+
// "Add 2 and 3.",
138+
// "The result is 5."
139+
// ]
140+
// },
141+
// metadata: { requestId: "unique-id", finishReason: "stop", .... }
142+
//
120143
// If the model returns invalid JSON or fails schema validation,
121144
// `llm.chat(...)` throws a StructuredOutputError with `code` and `validation` details.
122145
```

index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import ResilientLLM from "./lib/ResilientLLM.js";
22
import ProviderRegistry from "./lib/ProviderRegistry.js";
3-
import {
4-
StructuredOutputError,
5-
} from "./lib/StructuredOutput.js";
3+
import { ResilientLLMError } from "./lib/ResilientLLMError.js";
64

75
export {
86
ResilientLLM,
97
ProviderRegistry,
10-
StructuredOutputError
8+
ResilientLLMError,
119
};
1210

1311
export type {
@@ -20,15 +18,12 @@ export type {
2018
ChatResponse,
2119
ChatToolCallResult,
2220
SchemaValidationIssue,
23-
StructuredOutputErrorInfo,
2421
} from "./lib/ResilientLLM.js";
2522

2623
export type {
2724
ParseMode,
2825
ValidationMode,
2926
StructuredOutputResult,
30-
StructuredOutputErrorCode,
31-
StructuredOutputErrorInfo as StructuredOutputErrorDetail,
3227
NormalizedStructuredOutputConfig,
3328
StructuredRequestFields,
3429
ResponseEnvelope,
@@ -54,6 +49,10 @@ export type {
5449
RateLimitConfig,
5550
} from "./lib/RateLimitManager.js";
5651

52+
export type {
53+
ResilientLLMErrorCode,
54+
} from "./lib/ResilientLLMError.js";
55+
5756
export type {
5857
CircuitBreakerConfig,
5958
CircuitBreakerStatus,

lib/ResilientLLM.ts

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)