@careob/llm-gateway 1.0.0 → 1.1.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/README.md +202 -0
- package/dist/gateway.d.ts +3 -7
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +242 -64
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +2 -1
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +7 -2
- package/dist/key-pool.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +6 -4
- package/dist/middleware.js.map +1 -1
- package/dist/registry.d.ts +21 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +200 -9
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +37 -9
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @careob/llm-gateway
|
|
2
|
+
|
|
3
|
+
Lightweight LLM gateway for Node.js with multi-key rotation, rate-limit handling, provider failover, and spend tracking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **12+ providers** — OpenAI, Anthropic, Gemini, OpenRouter, DeepSeek, Groq, Mistral, Together, Qwen, Zhipu, Moonshot, Yi
|
|
8
|
+
- **Key rotation** — round-robin across multiple API keys per provider
|
|
9
|
+
- **Rate-limit handling** — automatic cooldown, exponential backoff with jitter on 429s
|
|
10
|
+
- **Retry classification** — 400s fail fast, 401/403 quarantine keys, 429/5xx retry with backoff
|
|
11
|
+
- **Provider failover** — capability-aware fallback to alternate providers
|
|
12
|
+
- **Safe errors** — API keys and raw provider bodies are never exposed in error messages
|
|
13
|
+
- **AbortSignal** — cancel requests with a caller-provided `AbortSignal`
|
|
14
|
+
- **Spend tracking** — per-request cost estimation and usage logging
|
|
15
|
+
- **Streaming** — SSE streaming with stream-idle timeout detection
|
|
16
|
+
- **Express middleware** — drop-in proxy for Express apps
|
|
17
|
+
- **Auto-discovery** — detects API keys from environment variables
|
|
18
|
+
- **Zero dependencies** — only uses `express` as an optional peer dependency
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @careob/llm-gateway
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { LLMGateway } from '@careob/llm-gateway';
|
|
30
|
+
|
|
31
|
+
const gateway = new LLMGateway({
|
|
32
|
+
keys: [
|
|
33
|
+
'sk-proj-your-openai-key',
|
|
34
|
+
{ key: 'sk-ant-your-anthropic-key', provider: 'anthropic' },
|
|
35
|
+
],
|
|
36
|
+
defaultModel: 'gpt-4o',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const response = await gateway.chat({
|
|
40
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(response.choices[0].message.content);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Auto-discover keys from environment
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Reads OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.
|
|
50
|
+
const gateway = LLMGateway.fromEnv({ defaultModel: 'gpt-4o' });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Multi-Key Rotation
|
|
54
|
+
|
|
55
|
+
Pass multiple keys for the same provider to distribute load:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const gateway = new LLMGateway({
|
|
59
|
+
keys: [
|
|
60
|
+
{ key: 'sk-proj-key-1', provider: 'openai', label: 'prod-1' },
|
|
61
|
+
{ key: 'sk-proj-key-2', provider: 'openai', label: 'prod-2' },
|
|
62
|
+
{ key: 'sk-proj-key-3', provider: 'openai', label: 'prod-3' },
|
|
63
|
+
],
|
|
64
|
+
defaultModel: 'gpt-4o',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Keys are rotated round-robin. When a key hits a 429, it's automatically cooled down and the next key is used.
|
|
69
|
+
|
|
70
|
+
## Streaming
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
for await (const chunk of gateway.chatStream({
|
|
74
|
+
model: 'gpt-4o',
|
|
75
|
+
messages: [{ role: 'user', content: 'Write a poem' }],
|
|
76
|
+
})) {
|
|
77
|
+
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## OpenRouter
|
|
82
|
+
|
|
83
|
+
Access 200+ models through a single API key via [OpenRouter](https://openrouter.ai):
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const gateway = new LLMGateway({
|
|
87
|
+
keys: ['sk-or-v1-your-openrouter-key'],
|
|
88
|
+
defaultModel: 'openai/gpt-4o',
|
|
89
|
+
openRouterReferer: 'https://your-app.com',
|
|
90
|
+
openRouterTitle: 'Your App Name',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Use namespaced model identifiers
|
|
94
|
+
const response = await gateway.chat({
|
|
95
|
+
model: 'anthropic/claude-sonnet-4',
|
|
96
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
OpenRouter keys (`sk-or-*`) are auto-detected. Models use namespaced identifiers like `openai/gpt-4o`, `anthropic/claude-sonnet-4`, `google/gemini-2.5-flash`, etc.
|
|
101
|
+
|
|
102
|
+
## Express Proxy
|
|
103
|
+
|
|
104
|
+
Mount the gateway as an API endpoint in your Express app:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import express from 'express';
|
|
108
|
+
import { LLMGateway, createExpressProxy } from '@careob/llm-gateway';
|
|
109
|
+
|
|
110
|
+
const app = express();
|
|
111
|
+
app.use(express.json());
|
|
112
|
+
|
|
113
|
+
const gateway = LLMGateway.fromEnv({ defaultModel: 'gpt-4o' });
|
|
114
|
+
const proxy = createExpressProxy(gateway, {
|
|
115
|
+
authorize: (req) => req.headers['x-api-key'] === 'your-secret',
|
|
116
|
+
extractMetadata: (req) => ({ userId: req.headers['x-user-id'] as string }),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.post('/v1/chat/completions', proxy.chatCompletions);
|
|
120
|
+
app.get('/v1/health', proxy.health);
|
|
121
|
+
|
|
122
|
+
app.listen(3000);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Spend Tracking
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const gateway = new LLMGateway({
|
|
129
|
+
keys: ['sk-proj-...'],
|
|
130
|
+
defaultModel: 'gpt-4o',
|
|
131
|
+
onUsage: (log) => {
|
|
132
|
+
console.log(`${log.model} | ${log.totalTokens} tokens | $${log.estimatedCostUsd.toFixed(6)}`);
|
|
133
|
+
// Save to your database, send to analytics, etc.
|
|
134
|
+
},
|
|
135
|
+
onAllKeysExhausted: (provider) => {
|
|
136
|
+
console.warn(`All ${provider} keys exhausted!`);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
| Option | Default | Description |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `keys` | *required* | Array of API key strings or `KeyInput` objects |
|
|
146
|
+
| `defaultModel` | — | Model to use when not specified in request |
|
|
147
|
+
| `maxRetries` | `3` | Retries across keys before failing |
|
|
148
|
+
| `cooldownMs` | `60000` | Cooldown duration for rate-limited keys (ms) |
|
|
149
|
+
| `timeoutMs` | `30000` | Connection timeout (ms) |
|
|
150
|
+
| `streamTimeoutMs` | `15000` | Stream idle timeout — no data received (ms) |
|
|
151
|
+
| `onUsage` | — | Callback for usage/spend logging |
|
|
152
|
+
| `onAllKeysExhausted` | — | Callback when all keys for a provider are exhausted |
|
|
153
|
+
| `openRouterReferer` | — | Your site URL for OpenRouter rankings (sent as `HTTP-Referer`) |
|
|
154
|
+
| `openRouterTitle` | — | Your app name for OpenRouter rankings (sent as `X-Title`) |
|
|
155
|
+
|
|
156
|
+
### KeyInput
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
{
|
|
160
|
+
key: string;
|
|
161
|
+
provider?: string; // Auto-detected from key prefix if omitted
|
|
162
|
+
rpm?: number; // Requests per minute limit
|
|
163
|
+
tpm?: number; // Tokens per minute limit
|
|
164
|
+
label?: string; // Human-readable label (e.g. "prod-key-1")
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Supported Providers
|
|
169
|
+
|
|
170
|
+
| Provider | Format | Models |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| OpenAI | openai | gpt-4o, gpt-4o-mini, o1, o3-mini, o4-mini, ... |
|
|
173
|
+
| Anthropic | anthropic | claude-opus-4, claude-sonnet-4, claude-haiku-3.5 |
|
|
174
|
+
| Google Gemini | gemini | gemini-2.5-pro, gemini-2.5-flash, ... |
|
|
175
|
+
| OpenRouter | openai | openai/gpt-4o, anthropic/claude-sonnet-4, google/gemini-2.5-flash, ... |
|
|
176
|
+
| DeepSeek | openai | deepseek-chat, deepseek-coder, deepseek-reasoner |
|
|
177
|
+
| Groq | openai | llama-3.3-70b, mixtral-8x7b, ... |
|
|
178
|
+
| Mistral | openai | mistral-large, mistral-small, codestral |
|
|
179
|
+
| Together AI | openai | Llama-3.3-70B, DeepSeek-R1, ... |
|
|
180
|
+
| Qwen | openai | qwen-turbo, qwen-plus, qwen-max |
|
|
181
|
+
| Zhipu | openai | glm-4, glm-4-flash, glm-4-plus |
|
|
182
|
+
| Moonshot | openai | moonshot-v1-8k/32k/128k |
|
|
183
|
+
| Yi | openai | yi-large, yi-medium, yi-spark |
|
|
184
|
+
|
|
185
|
+
## Health Check
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const stats = gateway.getStats();
|
|
189
|
+
// [{ provider: 'openai', label: 'prod-1', healthy: true, requestsThisMinute: 12, ... }]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm test # run tests
|
|
196
|
+
npm run test:coverage # run tests with coverage report
|
|
197
|
+
npm run ci # typecheck + build + test
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
package/dist/gateway.d.ts
CHANGED
|
@@ -3,15 +3,11 @@ export declare class LLMGateway {
|
|
|
3
3
|
private pool;
|
|
4
4
|
private config;
|
|
5
5
|
private providersByKey;
|
|
6
|
+
private openRouterDef;
|
|
6
7
|
constructor(config: GatewayConfig);
|
|
7
|
-
/**
|
|
8
|
-
* Auto-discover keys from environment variables.
|
|
9
|
-
* Scans for OPENAI_API_KEY, ANTHROPIC_KEY, GEMINI_API_KEY_1, DEEPSEEK_KEY, etc.
|
|
10
|
-
*/
|
|
11
8
|
static fromEnv(overrides?: Omit<GatewayConfig, 'keys'>, env?: Record<string, string | undefined>): LLMGateway;
|
|
12
9
|
chat(request: ChatRequest): Promise<ChatResponse>;
|
|
13
10
|
chatStream(request: ChatRequest): AsyncGenerator<StreamChunk, void, unknown>;
|
|
14
|
-
/** Get health/stats for all registered keys */
|
|
15
11
|
getStats(): {
|
|
16
12
|
provider: string;
|
|
17
13
|
label: string;
|
|
@@ -21,10 +17,10 @@ export declare class LLMGateway {
|
|
|
21
17
|
totalErrors: number;
|
|
22
18
|
cooldownUntil: number;
|
|
23
19
|
}[];
|
|
24
|
-
/** List all registered providers */
|
|
25
20
|
getProviders(): string[];
|
|
26
21
|
private registerKeys;
|
|
27
|
-
private
|
|
22
|
+
private getProviderDef;
|
|
23
|
+
private resolveTargets;
|
|
28
24
|
private logUsage;
|
|
29
25
|
}
|
|
30
26
|
export type GatewayErrorCode = 'CONFIG_ERROR' | 'PROVIDER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'ALL_EXHAUSTED' | 'UNSUPPORTED';
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,aAAa,EAEb,WAAW,EACX,YAAY,EACZ,WAAW,EAIZ,MAAM,YAAY,CAAC;AAEpB,qBAAa,UAAU;IACrB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAyG;IACvH,OAAO,CAAC,cAAc,CAAkC;IACxD,OAAO,CAAC,aAAa,CAA4B;gBAErC,MAAM,EAAE,aAAa;IAYjC,MAAM,CAAC,OAAO,CACZ,SAAS,GAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAM,EAC3C,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,UAAU;IAWP,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAkGhD,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,CAAC;IAuInF,QAAQ;;;;;;;;;IAIR,YAAY;IAMZ,OAAO,CAAC,YAAY;IAqCpB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,cAAc;IA4CtB,OAAO,CAAC,QAAQ;CAoCjB;AA6FD,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,gBAAgB,GAChB,eAAe,GACf,SAAS,GACT,eAAe,GACf,aAAa,CAAC;AAElB,qBAAa,YAAa,SAAQ,KAAK;IAG5B,IAAI,EAAE,gBAAgB;IACtB,UAAU,CAAC,EAAE,MAAM;gBAF1B,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,gBAAgB,EACtB,UAAU,CAAC,EAAE,MAAM,YAAA;CAK7B"}
|