@imenam/unified-llm-client 2.2.0 → 2.3.1
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 +21 -21
- package/README.md +383 -359
- package/dist/adapters/openrouter/OpenRouterLogAdapter.d.ts +11 -0
- package/dist/adapters/openrouter/OpenRouterLogAdapter.d.ts.map +1 -0
- package/dist/adapters/openrouter/OpenRouterLogAdapter.js +51 -0
- package/dist/adapters/openrouter/OpenRouterOutputAdapter.d.ts +6 -0
- package/dist/adapters/openrouter/OpenRouterOutputAdapter.d.ts.map +1 -0
- package/dist/adapters/openrouter/OpenRouterOutputAdapter.js +9 -0
- package/dist/adapters/openrouter/OpenRouterPromptAdapter.d.ts +15 -0
- package/dist/adapters/openrouter/OpenRouterPromptAdapter.d.ts.map +1 -0
- package/dist/adapters/openrouter/OpenRouterPromptAdapter.js +395 -0
- package/dist/adapters/openrouter/OpenRouterToolAdapter.d.ts +6 -0
- package/dist/adapters/openrouter/OpenRouterToolAdapter.d.ts.map +1 -0
- package/dist/adapters/openrouter/OpenRouterToolAdapter.js +16 -0
- package/dist/common/types.d.ts +10 -1
- package/dist/common/types.d.ts.map +1 -1
- package/dist/core/LLMAdapter.d.ts.map +1 -1
- package/dist/core/LLMAdapter.js +16 -0
- package/dist/core/Models.d.ts +6 -0
- package/dist/core/Models.d.ts.map +1 -1
- package/dist/core/Models.js +9 -0
- package/dist/core/UnifiedLLMClient.d.ts +2 -2
- package/dist/core/UnifiedLLMClient.d.ts.map +1 -1
- package/dist/core/UnifiedLLMClient.js +1 -1
- package/dist/examples/audio-example.js +1 -1
- package/dist/examples/example.js +16 -16
- package/dist/examples/qwen-omni-audio-example.js +5 -5
- package/dist/examples/qwen-vl-example.js +7 -7
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/package.json +71 -71
package/README.md
CHANGED
|
@@ -1,359 +1,383 @@
|
|
|
1
|
-
# @imenam/unified-llm-client
|
|
2
|
-
|
|
3
|
-
A unified interface for multiple LLM providers — OpenAI, Gemini, Anthropic, and Qwen — with built-in support for tool-calling, streaming, multimodal inputs, audio generation, and a proxy mode for API key isolation.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Unified API** — one interface, four providers
|
|
8
|
-
- **Tool-calling** — automatic execution loop with structured results
|
|
9
|
-
- **Streaming** — chunk-by-chunk responses via callbacks
|
|
10
|
-
- **Multimodal** — image and audio file inputs
|
|
11
|
-
- **Audio generation** — Text-to-Speech (Gemini)
|
|
12
|
-
- **Proxy mode** — delegate LLM calls to a server, no API keys on the client
|
|
13
|
-
- **API key resolver** — dynamically resolve API keys per request from a user-provided context
|
|
14
|
-
- **Cancellation** — native `AbortSignal` support
|
|
15
|
-
- **Usage tracking** — token counts and estimated costs
|
|
16
|
-
- **Full TypeScript support** — exported types and `.d.ts` declarations
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install @imenam/unified-llm-client
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Supported Providers
|
|
25
|
-
|
|
26
|
-
| Provider | Chat | Streaming | Tool-calling | Multimodal | Audio TTS |
|
|
27
|
-
|-----------|:----:|:---------:|:------------:|:----------:|:---------:|
|
|
28
|
-
| OpenAI | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
29
|
-
| Gemini | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
30
|
-
| Anthropic | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
31
|
-
| Qwen | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
MODELS.
|
|
69
|
-
MODELS.
|
|
70
|
-
MODELS.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
MODELS.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
import
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const client = new UnifiedLLMClient(MODELS.
|
|
226
|
-
proxyUrl: "http://localhost:3000",
|
|
227
|
-
proxyApiKey: "my-secret",
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1
|
+
# @imenam/unified-llm-client
|
|
2
|
+
|
|
3
|
+
A unified interface for multiple LLM providers — OpenAI, Gemini, Anthropic, and Qwen — with built-in support for tool-calling, streaming, multimodal inputs, audio generation, and a proxy mode for API key isolation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Unified API** — one interface, four providers
|
|
8
|
+
- **Tool-calling** — automatic execution loop with structured results
|
|
9
|
+
- **Streaming** — chunk-by-chunk responses via callbacks
|
|
10
|
+
- **Multimodal** — image and audio file inputs
|
|
11
|
+
- **Audio generation** — Text-to-Speech (Gemini)
|
|
12
|
+
- **Proxy mode** — delegate LLM calls to a server, no API keys on the client
|
|
13
|
+
- **API key resolver** — dynamically resolve API keys per request from a user-provided context
|
|
14
|
+
- **Cancellation** — native `AbortSignal` support
|
|
15
|
+
- **Usage tracking** — token counts and estimated costs
|
|
16
|
+
- **Full TypeScript support** — exported types and `.d.ts` declarations
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @imenam/unified-llm-client
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Supported Providers
|
|
25
|
+
|
|
26
|
+
| Provider | Chat | Streaming | Tool-calling | Multimodal | Audio TTS |
|
|
27
|
+
|-----------|:----:|:---------:|:------------:|:----------:|:---------:|
|
|
28
|
+
| OpenAI | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
29
|
+
| Gemini | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
30
|
+
| Anthropic | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
31
|
+
| Qwen | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
32
|
+
| OpenRouter | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
Set your API keys in a `.env` file:
|
|
37
|
+
|
|
38
|
+
```env
|
|
39
|
+
OPENAI_API_KEY=sk-...
|
|
40
|
+
GOOGLE_API_KEY=...
|
|
41
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
42
|
+
ALIBABA_CLOUD_API_KEY=... # for Qwen
|
|
43
|
+
OPEN_ROUTER_API_KEY=sk-or-... # for OpenRouter (OPENROUTER_API_KEY is also read)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
48
|
+
|
|
49
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI);
|
|
50
|
+
|
|
51
|
+
const response = await client.chat({
|
|
52
|
+
message: "What is the capital of France?",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log(response.content); // "The capital of France is Paris."
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Available Models
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { MODELS } from "@imenam/unified-llm-client";
|
|
62
|
+
|
|
63
|
+
// OpenAI
|
|
64
|
+
MODELS.GPT_4O_MINI
|
|
65
|
+
MODELS.GPT_5
|
|
66
|
+
|
|
67
|
+
// Gemini
|
|
68
|
+
MODELS.GEMINI_FLASH_2_5
|
|
69
|
+
MODELS.GEMINI_2_5_PRO
|
|
70
|
+
MODELS.GEMINI_3_FLASH
|
|
71
|
+
MODELS.GEMINI_3_PRO
|
|
72
|
+
MODELS.GEMINI_3_1_PRO
|
|
73
|
+
|
|
74
|
+
// Anthropic
|
|
75
|
+
MODELS.CLAUDE_SONNET_4_6
|
|
76
|
+
MODELS.CLAUDE_OPUS_4_6
|
|
77
|
+
|
|
78
|
+
// Qwen
|
|
79
|
+
MODELS.QWEN_VL_3
|
|
80
|
+
MODELS.QWEN_VL_FLASH
|
|
81
|
+
MODELS.QWEN_VL_3_FLASH
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You can also pass a custom model:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const client = new UnifiedLLMClient({
|
|
88
|
+
provider: "openai",
|
|
89
|
+
modelName: "gpt-4o",
|
|
90
|
+
costsPer1MTokens: { prompt: 2.5, output: 10 },
|
|
91
|
+
currency: "USD",
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## OpenRouter
|
|
96
|
+
|
|
97
|
+
OpenRouter gives access to every model of its catalogue with a single key. Build the
|
|
98
|
+
model from its OpenRouter id:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { UnifiedLLMClient, openRouterModel } from "@imenam/unified-llm-client";
|
|
102
|
+
|
|
103
|
+
const client = new UnifiedLLMClient(openRouterModel("google/gemini-3-flash-preview"));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Images are sent as `image_url`, PDFs as `file` (the model reads them natively when it can),
|
|
107
|
+
`.mp3` / `.wav` as `input_audio`.
|
|
108
|
+
- `response.usage.cost` holds the real cost of the call in USD, as reported by OpenRouter.
|
|
109
|
+
- `config.thinkingBudget` (> 0) is sent as `reasoning.max_tokens`.
|
|
110
|
+
- A failed generation throws an error that contains the model's native finish reason,
|
|
111
|
+
e.g. `OpenRouter: generation failed (MALFORMED_FUNCTION_CALL)`.
|
|
112
|
+
- Multi-turn tool calls: copy `response.reasoningDetails` onto the assistant message you
|
|
113
|
+
send back (`{ role: "assistant", tool_calls, reasoningDetails }`). Reasoning models such as
|
|
114
|
+
Gemini 3 reject the next turn otherwise.
|
|
115
|
+
- Optional headers: `OPENROUTER_APP_URL` (`HTTP-Referer`) and `OPENROUTER_APP_NAME` (`X-Title`).
|
|
116
|
+
|
|
117
|
+
## Tool-calling
|
|
118
|
+
|
|
119
|
+
Define tools with an `executor` function — the client will call them automatically and loop until the LLM produces a final text response.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
123
|
+
import type { ToolDefinition } from "@imenam/unified-llm-client";
|
|
124
|
+
|
|
125
|
+
const tools: ToolDefinition[] = [
|
|
126
|
+
{
|
|
127
|
+
name: "get_weather",
|
|
128
|
+
description: "Get the current weather for a city",
|
|
129
|
+
parameters: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
city: { type: "string", description: "City name" },
|
|
133
|
+
},
|
|
134
|
+
required: ["city"],
|
|
135
|
+
},
|
|
136
|
+
executor: async ({ city }) => {
|
|
137
|
+
return `The weather in ${city} is sunny and 22°C.`;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5);
|
|
143
|
+
|
|
144
|
+
const response = await client.chat({
|
|
145
|
+
message: "What's the weather like in Paris?",
|
|
146
|
+
tools,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log(response.content);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Streaming
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI);
|
|
156
|
+
|
|
157
|
+
await client.chat({
|
|
158
|
+
message: "Tell me a short story.",
|
|
159
|
+
stream: true,
|
|
160
|
+
onChunk: (chunk) => {
|
|
161
|
+
process.stdout.write(chunk.contentChunk ?? "");
|
|
162
|
+
},
|
|
163
|
+
onFinalResponse: (final) => {
|
|
164
|
+
console.log("\n\nTotal tokens:", final.usage?.totalTokens);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Multimodal Inputs
|
|
170
|
+
|
|
171
|
+
Attach images or audio files directly to a message:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const response = await client.chat({
|
|
175
|
+
message: {
|
|
176
|
+
role: "user",
|
|
177
|
+
content: "What do you see in this image?",
|
|
178
|
+
attachedFile: "./photo.jpg",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Multiple files are also supported:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
await client.chat({
|
|
187
|
+
message: {
|
|
188
|
+
role: "user",
|
|
189
|
+
content: "Compare these two images.",
|
|
190
|
+
attachedFile: ["./image1.jpg", "./image2.png"],
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Audio Generation (Gemini)
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
199
|
+
|
|
200
|
+
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5);
|
|
201
|
+
|
|
202
|
+
const audio = await client.generateAudio({
|
|
203
|
+
text: "Hello, this is a test of text-to-speech.",
|
|
204
|
+
outputPath: "./output.wav",
|
|
205
|
+
voiceConfig: { voiceName: "Kore" },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Proxy Mode
|
|
210
|
+
|
|
211
|
+
Run an HTTP server that holds your API keys server-side, and connect clients without any keys:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { ProxyServer } from "@imenam/unified-llm-client";
|
|
215
|
+
|
|
216
|
+
// Server side — API keys are read from process.env
|
|
217
|
+
const server = new ProxyServer({ port: 3000, apiKey: "my-secret" });
|
|
218
|
+
await server.start();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Client side — no API keys needed
|
|
223
|
+
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
224
|
+
|
|
225
|
+
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5, {
|
|
226
|
+
proxyUrl: "http://localhost:3000",
|
|
227
|
+
proxyApiKey: "my-secret",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// The chat() API is identical — streaming and file attachments are fully supported
|
|
231
|
+
const response = await client.chat({ message: "Hello!" });
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The proxy server also supports an `apiKeyResolver` for per-request dynamic key resolution (e.g. multi-tenant scenarios):
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { ProxyServer } from "@imenam/unified-llm-client";
|
|
238
|
+
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
|
|
239
|
+
|
|
240
|
+
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) =>
|
|
241
|
+
(await userKeyStore.get(ctx.userId)) ?? false;
|
|
242
|
+
|
|
243
|
+
const server = new ProxyServer({ port: 3000, apiKey: "my-secret", apiKeyResolver: resolver });
|
|
244
|
+
await server.start();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Client passes the context — server resolves the key
|
|
249
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
|
|
250
|
+
proxyUrl: "http://localhost:3000",
|
|
251
|
+
proxyApiKey: "my-secret",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const response = await client.chat({
|
|
255
|
+
message: "Hello!",
|
|
256
|
+
apiKeyContext: { userId: "user-123" },
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## API Key Resolver
|
|
261
|
+
|
|
262
|
+
Dynamically resolve which API key to use for each request based on a custom context (e.g. per-user key, request metadata):
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
266
|
+
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
|
|
267
|
+
|
|
268
|
+
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) => {
|
|
269
|
+
const key = await userKeyStore.get(ctx.userId);
|
|
270
|
+
return key ?? false; // return false to deny access
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
|
|
274
|
+
apiKeyResolver: resolver,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const response = await client.chat({
|
|
278
|
+
message: "Hello!",
|
|
279
|
+
apiKeyContext: { userId: "user-123" },
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
You can also set a global default resolver for all instances:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
UnifiedLLMClient.setDefaultApiKeyResolver((ctx) => {
|
|
287
|
+
return ctx.apiKey ?? false;
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
> The resolver is called on every `chat()` call. Returning `false` throws an access-denied error.
|
|
292
|
+
> The client-side `apiKeyResolver` option is for direct mode only. In proxy mode, the resolver is defined on the `ProxyServer` — the client passes `apiKeyContext` and the server resolves the key.
|
|
293
|
+
|
|
294
|
+
## Cancellation
|
|
295
|
+
|
|
296
|
+
Pass an `AbortSignal` to cancel a request mid-flight:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const ac = new AbortController();
|
|
300
|
+
setTimeout(() => ac.abort(), 800);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await client.chat({
|
|
304
|
+
message: "Write a very long story...",
|
|
305
|
+
stream: true,
|
|
306
|
+
signal: ac.signal,
|
|
307
|
+
onChunk: (chunk) => process.stdout.write(chunk.contentChunk ?? ""),
|
|
308
|
+
});
|
|
309
|
+
} catch (e) {
|
|
310
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
311
|
+
console.log("Request cancelled.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Logging
|
|
317
|
+
|
|
318
|
+
Pass a `logger` callback to capture structured logs for every request:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
|
|
322
|
+
logger: (log) => {
|
|
323
|
+
console.log(`[${log.provider}] ${log.model} — ${log.usage.totalTokens} tokens`);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Set a global default logger applied to all instances:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
UnifiedLLMClient.setDefaultLogger((log) => {
|
|
332
|
+
console.log(`[${log.provider}] ${log.model} — ${log.usage.totalTokens} tokens`);
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## API Reference
|
|
337
|
+
|
|
338
|
+
### `new UnifiedLLMClient(model, options?)`
|
|
339
|
+
|
|
340
|
+
| Parameter | Type | Description |
|
|
341
|
+
|-----------|------|-------------|
|
|
342
|
+
| `model` | `Model` | Model object from `MODELS` or a custom `{ provider, modelName, costsPer1MTokens, currency }` |
|
|
343
|
+
| `options.logger` | `(log: UnifiedLog) => void` | Optional logging callback |
|
|
344
|
+
| `options.proxyUrl` | `string` | Proxy server URL — activates proxy mode |
|
|
345
|
+
| `options.proxyApiKey` | `string` | Key sent in the `x-api-key` header (proxy mode) |
|
|
346
|
+
| `options.apiKey` | `string` | Static API key passed directly to the provider (overrides env var) |
|
|
347
|
+
| `options.apiKeyResolver` | `ApiKeyResolver` | Dynamic per-request key resolver (direct mode only — in proxy mode, set this on `ProxyServer`) |
|
|
348
|
+
|
|
349
|
+
### `client.chat(options)`
|
|
350
|
+
|
|
351
|
+
| Option | Type | Default | Description |
|
|
352
|
+
|--------|------|---------|-------------|
|
|
353
|
+
| `message` | `string \| ChatMessage \| ChatMessage[]` | — | User message or full conversation history |
|
|
354
|
+
| `tools` | `ToolDefinition[]` | `[]` | Tools available to the LLM |
|
|
355
|
+
| `toolChoice` | `"auto" \| "none" \| string` | `"auto"` | Tool selection strategy |
|
|
356
|
+
| `stream` | `boolean` | `false` | Enable streaming |
|
|
357
|
+
| `onChunk` | `(chunk: StreamedChatChunk) => void` | — | Streaming chunk callback |
|
|
358
|
+
| `onFinalResponse` | `(res: ChatResponse) => void` | — | Final response callback |
|
|
359
|
+
| `onLog` | `(log: UnifiedLog) => void` | — | Per-call log callback |
|
|
360
|
+
| `config` | `ChatOptionalConfig` | — | Advanced options: `thinkingBudget`, `metadata` |
|
|
361
|
+
| `signal` | `AbortSignal` | — | Cancellation signal |
|
|
362
|
+
| `apiKeyContext` | `unknown` | — | Context passed to `apiKeyResolver` to resolve the API key |
|
|
363
|
+
|
|
364
|
+
### `ChatResponse`
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
interface ChatResponse {
|
|
368
|
+
content: string | null;
|
|
369
|
+
toolCalls?: ToolCall[];
|
|
370
|
+
usage?: {
|
|
371
|
+
promptTokens: number;
|
|
372
|
+
completionTokens: number;
|
|
373
|
+
totalTokens: number;
|
|
374
|
+
};
|
|
375
|
+
finishReason?: string;
|
|
376
|
+
requestId?: string;
|
|
377
|
+
raw: unknown;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|