ai-resilience-gateway 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vasil Tomov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # ai-resilience-gateway
2
+
3
+ Resilient LLM chat completions with **provider fallback**, **retries**, and **timeouts**.
4
+ Zero runtime dependencies — a TypeScript library for learning production LLM client patterns and showcasing them in a portfolio.
5
+
6
+ When your primary provider rate-limits you or goes down, the gateway falls through to the next one so a single hiccup doesn’t take down your feature.
7
+
8
+ ```
9
+ Groq ──fails──▶ Ollama (local)
10
+ │ │
11
+ retries+backoff retries+backoff
12
+ ```
13
+
14
+ Presets also include `openrouter()` if you set `OPENROUTER_API_KEY`. You can order any providers you want, for example `[groq(), openrouter(), ollama()]`.
15
+
16
+ ## Why
17
+
18
+ Most “call an LLM” code is a single `fetch` with no retry, no timeout, and no plan B:
19
+
20
+ - **429** during traffic spikes → the feature just errors out
21
+ - a **provider outage** → downtime for anything LLM-backed
22
+ - a **hung request** with no timeout → work piles up until the process struggles
23
+
24
+ This library handles all three:
25
+
26
+ - exponential backoff on retryable failures (`429` / `5xx` / network / timeout)
27
+ - a hard per-request timeout via `AbortController`
28
+ - ordered fallback across any number of OpenAI-compatible providers
29
+
30
+ ## Requirements
31
+
32
+ - **Node 18+** (uses the global `fetch` API)
33
+
34
+ ## Setup
35
+
36
+ ```sh
37
+ git clone https://github.com/vtomov90/AI-resilience-gateway.git
38
+ cd AI-resilience-gateway
39
+ npm install
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Install the package:
45
+
46
+ ```sh
47
+ npm install ai-resilience-gateway
48
+ ```
49
+
50
+
51
+ Then use it:
52
+
53
+
54
+ ```typescript
55
+ import { LLMCascade, groq, ollama } from "ai-resilience-gateway";
56
+
57
+
58
+ const llm = new LLMCascade([groq(), ollama()], {
59
+ retries: 2,
60
+ timeoutMs: 30_000,
61
+ onFallback: (provider, err) =>
62
+ console.warn(`[gateway] ${provider} failed:`, err.message),
63
+ });
64
+
65
+ const result = await llm.complete([
66
+ { role: "system", content: "You are a concise assistant." },
67
+ { role: "user", content: "Explain retries in one sentence." },
68
+ ]);
69
+
70
+
71
+ console.log(result.text); // model reply
72
+ console.log(result.provider); // "groq" or "ollama" — who answered
73
+ console.log(result.latencyMs); // e.g. 266
74
+ ```
75
+
76
+
77
+ ### Provider presets
78
+
79
+ Built-in factories (they just return a `Provider` object):
80
+
81
+ | Preset | Default model | API key |
82
+ |--------|---------------|---------|
83
+ | `groq()` | `openai/gpt-oss-20b` | `GROQ_API_KEY` |
84
+ | `openrouter()` | `meta-llama/llama-3.3-70b-instruct` | `OPENROUTER_API_KEY` (optional) |
85
+ | `ollama()` | `llama3.1` | none (local) |
86
+
87
+ Override model or key when needed:
88
+
89
+ ```typescript
90
+ groq("openai/gpt-oss-20b", process.env.GROQ_API_KEY);
91
+ openrouter("meta-llama/llama-3.3-70b-instruct", process.env.OPENROUTER_API_KEY);
92
+ ollama("llama3.1", "http://localhost:11434");
93
+ ```
94
+
95
+ ### Any OpenAI-compatible endpoint
96
+
97
+ ```typescript
98
+ const custom = {
99
+ name: "together",
100
+ url: "https://api.together.xyz/v1/chat/completions",
101
+ model: "some-model-id",
102
+ apiKey: process.env.TOGETHER_API_KEY,
103
+ };
104
+
105
+ const llm = new LLMCascade([custom, ollama()]);
106
+ ```
107
+
108
+ ## API
109
+
110
+ ### `new LLMCascade(providers, options?)`
111
+
112
+ | Option | Default | Description |
113
+ |--------|---------|-------------|
114
+ | `timeoutMs` | `30000` | Per-request timeout; abort is treated as retryable |
115
+ | `retries` | `2` | Attempts per provider (`1` = no retry) |
116
+ | `backoffMs` | `500` | Base backoff; doubles each retry (`500` → `1000` → …) |
117
+ | `onFallback` | — | `(provider, error) => void` when moving past a provider |
118
+
119
+ ### `cascade.complete(messages, options?)`
120
+
121
+ `options`: `{ temperature?, maxTokens?, signal? }`
122
+
123
+ Returns:
124
+
125
+ ```typescript
126
+ {
127
+ text: string;
128
+ provider: string;
129
+ model: string;
130
+ latencyMs: number;
131
+ usage?: { promptTokens?: number; completionTokens?: number };
132
+ }
133
+ ```
134
+
135
+ Throws `AllProvidersFailedError` (with a `.causes` map of `provider → Error`) only when every provider is exhausted.
136
+
137
+ ## Retry policy
138
+
139
+ **Retryable** (same provider, with backoff):
140
+
141
+ - HTTP `408`, `429`, `500`, `502`, `503`, `504`
142
+ - network errors and timeouts
143
+
144
+ **Non-retryable** (fall through to the next provider immediately):
145
+
146
+ - typical `4xx` like `401`, `403`, `404` — no point hammering a bad API key or unknown URL
147
+
148
+ ## Example CLI
149
+
150
+ Create a local `.env` (do **not** commit it):
151
+
152
+ ```env
153
+ GROQ_API_KEY=gsk_your_key_here
154
+ OPENROUTER_API_KEY=
155
+ OLLAMA_MODEL=llama3.1
156
+ OLLAMA_BASE_URL=http://localhost:11434
157
+ ```
158
+
159
+
160
+
161
+ Run:
162
+
163
+ ```sh
164
+ npm run example -- "Say hello in one sentence."
165
+ ```
166
+
167
+ The example tries Groq first (if `GROQ_API_KEY` is set), then Ollama as a local fallback.
168
+
169
+ > Groq model ids change over time and can depend on your account plan. If you get `model_not_found`, list models for your key (`GET https://api.groq.com/openai/v1/models`) and pass an available chat model into `groq("your-model-id")`.
170
+
171
+ ## Development
172
+
173
+ ```sh
174
+ npm test # Vitest — fetch fully mocked, no network / no API keys
175
+ npm run test:watch
176
+ npm run typecheck
177
+ npm run build
178
+ ```
179
+
180
+ ### Project layout
181
+
182
+ ```
183
+ src/
184
+ types.ts # ChatMessage, Provider, options, result types
185
+ errors.ts # HttpError, AllProvidersFailedError
186
+ retry.ts # isRetryable, backoff helpers
187
+ cascade.ts # LLMCascade (request → retries → fallback)
188
+ providers.ts # groq / openrouter / ollama presets
189
+ index.ts # public exports
190
+ examples/
191
+ ask.ts # live demo CLI
192
+ tests/ # unit tests with mocked fetch
193
+ ```
194
+
195
+ ## What this project demonstrates
196
+
197
+ - OpenAI-compatible chat completions (`choices[0].message.content`)
198
+ - `fetch` + `AbortController` for timeouts and external abort
199
+ - Retry vs fallback as separate layers
200
+ - Typed custom errors for debugging multi-provider failure
201
+ - Vitest with mocked `fetch` and fake timers
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { CascadeOptions, ChatMessage, CompletionOptions, CompletionResult, Provider } from "./types.js";
2
+ export declare class LLMCascade {
3
+ private readonly providers;
4
+ private readonly opts;
5
+ constructor(providers: Provider[], options?: CascadeOptions);
6
+ complete(messages: ChatMessage[], options?: CompletionOptions): Promise<CompletionResult>;
7
+ private completeWithRetries;
8
+ private request;
9
+ }
10
+ //# sourceMappingURL=cascade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoF;gBAE7F,SAAS,EAAE,QAAQ,EAAE,EAAE,OAAO,GAAE,cAAmB;IAczD,QAAQ,CACV,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAE,gBAAgB,CAAC;YAgBf,mBAAmB;YAsBnB,OAAO;CAqExB"}
@@ -0,0 +1,95 @@
1
+ import { AllProvidersFailedError, HttpError } from "./errors.js";
2
+ import { isRetryable, sleep } from "./retry.js";
3
+ export class LLMCascade {
4
+ providers;
5
+ opts;
6
+ constructor(providers, options = {}) {
7
+ if (providers.length === 0) {
8
+ throw new Error("LLMCascade requires at least one provider");
9
+ }
10
+ this.providers = providers;
11
+ this.opts = {
12
+ timeoutMs: options.timeoutMs ?? 30_000,
13
+ retries: options.retries ?? 2,
14
+ backoffMs: options.backoffMs ?? 500,
15
+ onFallback: options.onFallback,
16
+ };
17
+ }
18
+ async complete(messages, options = {}) {
19
+ const causes = {};
20
+ for (const provider of this.providers) {
21
+ try {
22
+ return await this.completeWithRetries(provider, messages, options);
23
+ }
24
+ catch (err) {
25
+ const error = err instanceof Error ? err : new Error(String(err));
26
+ causes[provider.name] = error;
27
+ this.opts.onFallback?.(provider.name, error);
28
+ }
29
+ }
30
+ throw new AllProvidersFailedError(causes);
31
+ }
32
+ async completeWithRetries(provider, messages, options) {
33
+ let lastError = new Error("unreachable");
34
+ for (let attempt = 0; attempt < this.opts.retries; attempt++) {
35
+ if (attempt > 0) {
36
+ await sleep(this.opts.backoffMs * 2 ** (attempt - 1));
37
+ }
38
+ try {
39
+ return await this.request(provider, messages, options);
40
+ }
41
+ catch (err) {
42
+ lastError = err instanceof Error ? err : new Error(String(err));
43
+ if (!isRetryable(lastError))
44
+ break;
45
+ }
46
+ }
47
+ throw lastError;
48
+ }
49
+ async request(provider, messages, options) {
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(new Error(`timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
52
+ options.signal?.addEventListener("abort", () => controller.abort(options.signal.reason), { once: true });
53
+ const started = Date.now();
54
+ try {
55
+ const res = await fetch(provider.url, {
56
+ method: "POST",
57
+ signal: controller.signal,
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}),
61
+ ...provider.headers,
62
+ },
63
+ body: JSON.stringify({
64
+ model: provider.model,
65
+ messages,
66
+ temperature: options.temperature ?? 0.7,
67
+ ...(options.maxTokens ? { max_tokens: options.maxTokens } : {})
68
+ }),
69
+ });
70
+ if (!res.ok) {
71
+ const body = await res.text().catch(() => "");
72
+ throw new HttpError(res.status, `${provider.name} HTTP ${res.status}: ${body.slice(0, 200)}`);
73
+ }
74
+ const data = (await res.json());
75
+ const text = data.choices?.[0]?.message?.content;
76
+ if (typeof text !== "string") {
77
+ throw new HttpError(502, `${provider.name} returned no completion text`);
78
+ }
79
+ return {
80
+ text,
81
+ provider: provider.name,
82
+ model: provider.model,
83
+ latencyMs: Date.now() - started,
84
+ usage: {
85
+ promptTokens: data.usage?.prompt_tokens,
86
+ completionTokens: data.usage?.completion_tokens,
87
+ }
88
+ };
89
+ }
90
+ finally {
91
+ clearTimeout(timer);
92
+ }
93
+ }
94
+ }
95
+ //# sourceMappingURL=cascade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cascade.js","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAShD,MAAM,OAAO,UAAU;IACF,SAAS,CAAa;IACtB,IAAI,CAAoF;IAEzG,YAAY,SAAqB,EAAE,UAA0B,EAAE;QAC3D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAC,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QACjE,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG;YACR,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,MAAM;YACtC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,CAAC;YAC7B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,GAAG;YACnC,UAAU,EAAE,OAAO,CAAC,UAAU;SACjC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,QAAQ,CACV,QAAuB,EACvB,UAA6B,EAAE;QAE/B,MAAM,MAAM,GAA0B,EAAE,CAAC;QAEzC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACpC,IAAG,CAAC;gBACA,OAAO,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YACvE,CAAC;YAAA,OAAO,GAAG,EAAC,CAAC;gBACT,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAClE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;gBAC9B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACjD,CAAC;QACL,CAAC;QAED,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAC7B,QAAkB,EAClB,QAAuB,EACvB,OAA0B;QAE1B,IAAI,SAAS,GAAU,IAAI,KAAK,CAAC,aAAa,CAAC,CAAA;QAE/C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAC,CAAC;YAC1D,IAAI,OAAO,GAAG,CAAC,EAAC,CAAC;gBACb,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1D,CAAC;YACD,IAAG,CAAC;gBACA,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC3D,CAAC;YAAA,OAAM,GAAG,EAAC,CAAC;gBACR,SAAS,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAChE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC;oBAAE,MAAM;YACvC,CAAC;QACL,CAAC;QAED,MAAM,SAAS,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,OAAO,CACjB,QAAkB,EAClB,QAAuB,EACvB,OAA0B;QAE1B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,iBAAiB,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAE3H,OAAO,CAAC,MAAM,EAAE,gBAAgB,CAC5B,OAAO,EACP,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,MAAO,CAAC,MAAM,CAAC,EAC9C,EAAE,IAAI,EAAE,IAAI,EAAE,CACjB,CAAC;QAGF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,IAAG,CAAC;YACA,MAAM,GAAG,GAAG,MAAM,KAAK,CAAE,QAAQ,CAAC,GAAG,EAAC;gBAClC,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,OAAO,EAAE;oBACL,cAAc,EAAE,kBAAkB;oBAClC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAG,aAAa,EAAE,UAAU,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC3E,GAAG,QAAQ,CAAC,OAAO;iBACtB;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACjB,KAAK,EAAE,QAAQ,CAAC,KAAK;oBACrB,QAAQ;oBACR,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,GAAG;oBACvC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAClE,CAAC;aACL,CAAC,CAAC;YAEH,IAAG,CAAC,GAAG,CAAC,EAAE,EAAC,CAAC;gBACR,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC9C,MAAM,IAAI,SAAS,CACf,GAAG,CAAC,MAAM,EACV,GAAG,QAAQ,CAAC,IAAI,SAAS,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC/D,CAAA;YACL,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAG7B,CAAA;YAGD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;YACjD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAC,CAAC;gBAC1B,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,IAAI,8BAA8B,CAAC,CAAC;YAC7E,CAAC;YAED,OAAO;gBACH,IAAI;gBACJ,QAAQ,EAAE,QAAQ,CAAC,IAAI;gBACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO;gBAC/B,KAAK,EAAE;oBACH,YAAY,EAAE,IAAI,CAAC,KAAK,EAAE,aAAa;oBACvC,gBAAgB,EAAE,IAAI,CAAC,KAAK,EAAE,iBAAiB;iBAClD;aACJ,CAAA;QACL,CAAC;gBAAQ,CAAC;YACN,YAAY,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;IAEL,CAAC;CAGJ"}
@@ -0,0 +1,9 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
5
+ export declare class AllProvidersFailedError extends Error {
6
+ readonly causes: Record<string, Error>;
7
+ constructor(causes: Record<string, Error>);
8
+ }
9
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,SAAU,SAAQ,KAAK;aAEZ,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAC,MAAM;CAKrB;AAGD,qBAAa,uBAAwB,SAAQ,KAAK;aAE1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;gBAA7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;CAUpD"}
package/dist/errors.js ADDED
@@ -0,0 +1,19 @@
1
+ export class HttpError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = "HttpError";
7
+ }
8
+ }
9
+ export class AllProvidersFailedError extends Error {
10
+ causes;
11
+ constructor(causes) {
12
+ super("All LLM providers failed: " +
13
+ Object.entries(causes)
14
+ .map(([name, err]) => `${name} (${err.message})`));
15
+ this.causes = causes;
16
+ this.name = "AllProvidersFailedError";
17
+ }
18
+ }
19
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEZ;IADpB,YACoB,MAAc,EAC9B,OAAc;QAEd,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QAI9B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC5B,CAAC;CACJ;AAGD,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAE1B;IADpB,YACoB,MAA6B;QAE7C,KAAK,CACD,4BAA4B;YACxB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;iBACjB,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,GAAG,CAAC,OAAO,GAAG,CAAC,CAC5D,CAAC;QANc,WAAM,GAAN,MAAM,CAAuB;QAQ7C,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAC1C,CAAC;CACJ"}
@@ -0,0 +1,6 @@
1
+ export { LLMCascade } from "./cascade.js";
2
+ export { AllProvidersFailedError, HttpError } from "./errors.js";
3
+ export { groq, ollama, openrouter } from "./providers.js";
4
+ export { isRetryable, RETRYABLE_STATUS, sleep } from "./retry.js";
5
+ export type { CascadeOptions, ChatMessage, CompletionOptions, CompletionResult, Provider, } from "./types.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EACV,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,GACT,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { LLMCascade } from "./cascade.js";
2
+ export { AllProvidersFailedError, HttpError } from "./errors.js";
3
+ export { groq, ollama, openrouter } from "./providers.js";
4
+ export { isRetryable, RETRYABLE_STATUS, sleep } from "./retry.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Provider } from "./types.js";
2
+ export declare const groq: (model?: string, apiKey?: string | undefined) => Provider;
3
+ export declare const openrouter: (model?: string, apiKey?: string | undefined) => Provider;
4
+ export declare const ollama: (model?: string, baseUrl?: string) => Provider;
5
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,eAAO,MAAM,IAAI,GACb,cAA4B,EAC5B,2BAAiC,KAClC,QAKD,CAAA;AAEF,eAAO,MAAM,UAAU,GACnB,cAA2C,EAC3C,2BAAuC,KACxC,QASD,CAAA;AAEF,eAAO,MAAM,MAAM,GACf,cAAkB,EAClB,gBAAkC,KACnC,QAID,CAAA"}
@@ -0,0 +1,22 @@
1
+ export const groq = (model = "openai/gpt-oss-20b", apiKey = process.env.GROQ_API_KEY) => ({
2
+ name: "groq",
3
+ url: "https://api.groq.com/openai/v1/chat/completions",
4
+ model,
5
+ apiKey
6
+ });
7
+ export const openrouter = (model = "meta-llama/llama-3.3-70b-instruct", apiKey = process.env.OPENROUTER_API_KEY) => ({
8
+ name: "openrouter",
9
+ url: "https://openrouter.ai/api/v1/chat/completions",
10
+ model,
11
+ apiKey,
12
+ headers: {
13
+ "HTTP-Referer": "https://github.com/ai-resilience-gateway",
14
+ "X-Title": "ai-resilience-gateway",
15
+ }
16
+ });
17
+ export const ollama = (model = "llama3.1", baseUrl = "http://localhost:11434") => ({
18
+ name: "ollama",
19
+ url: `${baseUrl}/v1/chat/completions`,
20
+ model,
21
+ });
22
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,IAAI,GAAG,CAChB,KAAK,GAAG,oBAAoB,EAC5B,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,EACzB,EAAE,CAAC,CAAC;IACZ,IAAI,EAAE,MAAM;IACZ,GAAG,EAAG,iDAAiD;IACvD,KAAK;IACL,MAAM;CACT,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CACtB,KAAK,GAAG,mCAAmC,EAC3C,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAC/B,EAAE,CAAC,CAAC;IACZ,IAAI,EAAG,YAAY;IACnB,GAAG,EAAE,+CAA+C;IACpD,KAAK;IACL,MAAM;IACN,OAAO,EAAE;QACL,cAAc,EAAE,0CAA0C;QAC1D,SAAS,EAAE,uBAAuB;KACrC;CACJ,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG,CAClB,KAAK,GAAG,UAAU,EAClB,OAAO,GAAG,wBAAwB,EAC1B,EAAE,CAAC,CAAC;IACZ,IAAI,EAAE,QAAQ;IACd,GAAG,EAAE,GAAG,OAAO,sBAAsB;IACrC,KAAK;CACR,CAAC,CAAA"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A set of HTTP status codes that indicate temporary or transient failures.
3
+ *
4
+ * If a provider fails with one of these statuses, it means the issue might resolve
5
+ * if we retry the request (e.g., rate limits cooling down or server recovering).
6
+ * Non-listed statuses (like 401 Unauthorized or 404 Not Found) are permanent and
7
+ * should fail immediately without retrying on the same provider.
8
+ */
9
+ export declare const RETRYABLE_STATUS: Set<number>;
10
+ /**
11
+ * Pauses execution for a specified duration.
12
+ *
13
+ * Uses a Promise wrapping `setTimeout` (forming a closure over `ms`) so it can
14
+ * be paused using `await`.
15
+ *
16
+ * @param ms - The number of milliseconds to pause execution.
17
+ * @returns A Promise that resolves after the specified delay.
18
+
19
+ */
20
+ export declare const sleep: (ms: number) => Promise<unknown>;
21
+ /**
22
+ * Determines whether a failed request should be retried on the same provider.
23
+ *
24
+ * @param err - The Error caught during a provider request attempt.
25
+ * @returns `true` if the failure is temporary and safe to retry; `false` otherwise.
26
+ */
27
+ export declare function isRetryable(err: Error): boolean;
28
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,eAAO,MAAM,gBAAgB,aAA2C,CAAC;AAIzE;;;;;;;;;EASE;AAEF,eAAO,MAAM,KAAK,GAAI,IAAI,MAAM,qBAA0C,CAAC;AAG3E;;;;;GAKG;AAEH,wBAAgB,WAAW,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAG/C"}
package/dist/retry.js ADDED
@@ -0,0 +1,33 @@
1
+ import { HttpError } from "./errors.js";
2
+ /**
3
+ * A set of HTTP status codes that indicate temporary or transient failures.
4
+ *
5
+ * If a provider fails with one of these statuses, it means the issue might resolve
6
+ * if we retry the request (e.g., rate limits cooling down or server recovering).
7
+ * Non-listed statuses (like 401 Unauthorized or 404 Not Found) are permanent and
8
+ * should fail immediately without retrying on the same provider.
9
+ */
10
+ export const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
11
+ /**
12
+ * Pauses execution for a specified duration.
13
+ *
14
+ * Uses a Promise wrapping `setTimeout` (forming a closure over `ms`) so it can
15
+ * be paused using `await`.
16
+ *
17
+ * @param ms - The number of milliseconds to pause execution.
18
+ * @returns A Promise that resolves after the specified delay.
19
+
20
+ */
21
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22
+ /**
23
+ * Determines whether a failed request should be retried on the same provider.
24
+ *
25
+ * @param err - The Error caught during a provider request attempt.
26
+ * @returns `true` if the failure is temporary and safe to retry; `false` otherwise.
27
+ */
28
+ export function isRetryable(err) {
29
+ if (err instanceof HttpError)
30
+ return RETRYABLE_STATUS.has(err.status);
31
+ return true;
32
+ }
33
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAIzE;;;;;;;;;EASE;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAG3E;;;;;GAKG;AAEH,MAAM,UAAU,WAAW,CAAC,GAAU;IAClC,IAAI,GAAG,YAAY,SAAS;QAAE,OAAO,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACrE,OAAO,IAAI,CAAC;AAChB,CAAC"}
@@ -0,0 +1,33 @@
1
+ export interface ChatMessage {
2
+ role: "system" | "user" | "assistant";
3
+ content: string;
4
+ }
5
+ export interface Provider {
6
+ name: string;
7
+ url: string;
8
+ model: string;
9
+ apiKey?: string;
10
+ headers?: Record<string, string>;
11
+ }
12
+ export interface CascadeOptions {
13
+ timeoutMs?: number;
14
+ retries?: number;
15
+ backoffMs?: number;
16
+ onFallback?: (provider: string, error: Error) => void;
17
+ }
18
+ export interface CompletionOptions {
19
+ temperature?: number;
20
+ maxTokens?: number;
21
+ signal?: AbortSignal;
22
+ }
23
+ export interface CompletionResult {
24
+ text: string;
25
+ provider: string;
26
+ model: string;
27
+ latencyMs: number;
28
+ usage?: {
29
+ promptTokens?: number;
30
+ completionTokens?: number;
31
+ };
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACzD;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAEhE"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "ai-resilience-gateway",
3
+ "version": "1.0.0",
4
+ "description": "Resilient LLM chat completions with provider fallback, retries, and timeouts. Zero runtime dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "typecheck": "tsc --noEmit -p tsconfig.json",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "example": "tsx examples/ask.ts",
23
+ "prepublishOnly": "npm test && npm run build"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "llm",
30
+ "openai-compatible",
31
+ "groq",
32
+ "openrouter",
33
+ "ollama",
34
+ "fallback",
35
+ "retry",
36
+ "typescript"
37
+ ],
38
+ "author": "Vasil Tomov",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/vtomov90/AI-resilience-gateway.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/vtomov90/AI-resilience-gateway/issues"
46
+ },
47
+ "homepage": "https://github.com/vtomov90/AI-resilience-gateway#readme",
48
+
49
+ "devDependencies": {
50
+ "@types/node": "^26.6.1",
51
+ "dotenv": "^17.4.2",
52
+ "tsx": "^4.23.13",
53
+ "typescript": "^5.7.2",
54
+ "vitest": "^4.1.11"
55
+ }
56
+ }