@imenam/unified-llm-client 2.3.1 → 3.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 +21 -21
- package/README.md +383 -383
- 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/package.json +69 -71
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 imenam
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 imenam
|
|
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
CHANGED
|
@@ -1,383 +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
|
-
| 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
|
|
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
|
|
@@ -81,7 +81,7 @@ function generateSimpleAudio(client) {
|
|
|
81
81
|
}
|
|
82
82
|
function generateMultiVoiceAudio(client) {
|
|
83
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
-
const podcastText = `Speaker 1: Bienvenue dans notre podcast technologique !
|
|
84
|
+
const podcastText = `Speaker 1: Bienvenue dans notre podcast technologique !
|
|
85
85
|
Speaker 2: Aujourd'hui, nous parlons d'intelligence artificielle et de génération de voix.`;
|
|
86
86
|
const multiOutputPath = path.join(AUDIO_OUTPUT_DIR, "podcast-example.wav");
|
|
87
87
|
const multiResponse = yield client.generateMultiVoiceAudio(podcastText, [
|
package/dist/examples/example.js
CHANGED
|
@@ -183,8 +183,8 @@ function basicAudioGeneration() {
|
|
|
183
183
|
console.log(voices.join(', '));
|
|
184
184
|
// Exemple 3: Génération multi-voix simple
|
|
185
185
|
console.log('\n🎙️ Génération multi-voix...');
|
|
186
|
-
const dialogText = `Speaker 1: Salut ! content ça va ?
|
|
187
|
-
Speaker 2: Très bien, merci ! Et toi ?
|
|
186
|
+
const dialogText = `Speaker 1: Salut ! content ça va ?
|
|
187
|
+
Speaker 2: Très bien, merci ! Et toi ?
|
|
188
188
|
Speaker 1: Ça va super bien, merci de demander !`;
|
|
189
189
|
const multiAudioResponse = yield client.generateMultiVoiceAudio(dialogText, [
|
|
190
190
|
{ speaker: "Speaker 1", voiceName: "Kore" },
|
|
@@ -326,20 +326,20 @@ function parseSubject() {
|
|
|
326
326
|
const res = yield client.chat({
|
|
327
327
|
message: {
|
|
328
328
|
role: "user",
|
|
329
|
-
content: `Voila le sujet du devoir. Peux-tu le parser en respectant la structure suivante ?
|
|
330
|
-
{
|
|
331
|
-
questions: [
|
|
332
|
-
{
|
|
333
|
-
number: "1",
|
|
334
|
-
content: "Question 1" or "Exercie N", etc...,
|
|
335
|
-
type: "Title" or "Question",
|
|
336
|
-
marking_scheme: "X" or "", // if type is "Question", X can be a float - 3.5, 4, etc...
|
|
337
|
-
sub_questions: [
|
|
338
|
-
...same structure
|
|
339
|
-
]
|
|
340
|
-
}
|
|
341
|
-
]
|
|
342
|
-
}
|
|
329
|
+
content: `Voila le sujet du devoir. Peux-tu le parser en respectant la structure suivante ?
|
|
330
|
+
{
|
|
331
|
+
questions: [
|
|
332
|
+
{
|
|
333
|
+
number: "1",
|
|
334
|
+
content: "Question 1" or "Exercie N", etc...,
|
|
335
|
+
type: "Title" or "Question",
|
|
336
|
+
marking_scheme: "X" or "", // if type is "Question", X can be a float - 3.5, 4, etc...
|
|
337
|
+
sub_questions: [
|
|
338
|
+
...same structure
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
343
|
`,
|
|
344
344
|
attachedFile: "./media/nested_subject.pdf"
|
|
345
345
|
},
|
|
@@ -56,13 +56,13 @@ const path = __importStar(require("path"));
|
|
|
56
56
|
* ALIBABA_CLOUD_API_KEY=... npm run example:qwen-omni -- ./chemin/audio.mp3 [--tools]
|
|
57
57
|
* npm run example:qwen-omni -- ./reponse.txt [duree_en_secondes]
|
|
58
58
|
*/
|
|
59
|
-
const TRANSCRIPTION_PROMPT = `Transcris cet extrait audio intégralement, en français.
|
|
60
|
-
Réponds UNIQUEMENT avec un tableau JSON, sans texte autour et sans bloc de code.
|
|
61
|
-
Chaque élément représente une phrase et suit ce format exact :
|
|
62
|
-
{"timecode": "MM:SS", "speaker": "nom ou Inconnu", "text": "la phrase transcrite"}
|
|
59
|
+
const TRANSCRIPTION_PROMPT = `Transcris cet extrait audio intégralement, en français.
|
|
60
|
+
Réponds UNIQUEMENT avec un tableau JSON, sans texte autour et sans bloc de code.
|
|
61
|
+
Chaque élément représente une phrase et suit ce format exact :
|
|
62
|
+
{"timecode": "MM:SS", "speaker": "nom ou Inconnu", "text": "la phrase transcrite"}
|
|
63
63
|
Le timecode est l'instant de DÉBUT de la phrase, relatif au début de l'extrait.`;
|
|
64
64
|
/** Prompt du mode function calling : le format est défini par le schéma de l'outil. */
|
|
65
|
-
const TRANSCRIPTION_PROMPT_TOOLS = `Transcris cet extrait audio intégralement, en français,
|
|
65
|
+
const TRANSCRIPTION_PROMPT_TOOLS = `Transcris cet extrait audio intégralement, en français,
|
|
66
66
|
puis transmets le résultat via l'outil submit_transcription.`;
|
|
67
67
|
const TRANSCRIPTION_TOOL = {
|
|
68
68
|
name: "submit_transcription",
|
|
@@ -144,13 +144,13 @@ function qwenVLBoundingBoxesExample() {
|
|
|
144
144
|
console.log("📦 Exemple 2: Extraction de bounding boxes...");
|
|
145
145
|
// Initialiser le client Qwen
|
|
146
146
|
const client = new UnifiedLLMClient_1.UnifiedLLMClient(Models_1.MODELS.QWEN_VL_3);
|
|
147
|
-
const expectedJsonFormat = `{
|
|
148
|
-
"data": [
|
|
149
|
-
{
|
|
150
|
-
"bbox_2d": [x1, y1, x2, y2],
|
|
151
|
-
"text_content": "contenu du texte"
|
|
152
|
-
}
|
|
153
|
-
]
|
|
147
|
+
const expectedJsonFormat = `{
|
|
148
|
+
"data": [
|
|
149
|
+
{
|
|
150
|
+
"bbox_2d": [x1, y1, x2, y2],
|
|
151
|
+
"text_content": "contenu du texte"
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
154
|
}`;
|
|
155
155
|
// Redimensionner l'image pour l'extraction de bounding boxes
|
|
156
156
|
const resizedImageBuffer = yield resizeImageForVision("./media/Gaspard_5_with_lines.png", 1000);
|
package/package.json
CHANGED
|
@@ -1,71 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@imenam/unified-llm-client",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A unified interface for multiple LLM providers (OpenAI, Gemini, Anthropic, Qwen) with tool-calling, streaming, and audio support",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"dist",
|
|
9
|
-
"README.md",
|
|
10
|
-
"LICENSE"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
14
|
-
"dev": "tsc -w",
|
|
15
|
-
"prepublishOnly": "npm run build",
|
|
16
|
-
"release:patch": "npm version patch && npm publish",
|
|
17
|
-
"release:minor": "npm version minor && npm publish",
|
|
18
|
-
"release:major": "npm version major && npm publish",
|
|
19
|
-
"example": "node dist/examples/example.js",
|
|
20
|
-
"example:audio": "node dist/examples/audio-example.js",
|
|
21
|
-
"example:qwen": "node dist/examples/qwen-vl-example.js",
|
|
22
|
-
"example:qwen-omni": "node dist/examples/qwen-omni-audio-example.js",
|
|
23
|
-
"example:anthropic": "node dist/examples/anthropic-example.js",
|
|
24
|
-
"example:abort": "node dist/examples/chat-abort-example.js",
|
|
25
|
-
"example:proxy": "node dist/examples/proxy-example.js",
|
|
26
|
-
"example:proxy-api-key-resolver": "node dist/examples/proxy-api-key-resolver-example.js",
|
|
27
|
-
"example:api-key-resolver": "node dist/examples/api-key-resolver-example.js",
|
|
28
|
-
"proxy:start": "node dist/examples/proxy-server-start.js",
|
|
29
|
-
"example:error-logging": "node dist/examples/example-error-logging.js"
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"llm",
|
|
33
|
-
"openai",
|
|
34
|
-
"gemini",
|
|
35
|
-
"anthropic",
|
|
36
|
-
"qwen",
|
|
37
|
-
"ai",
|
|
38
|
-
"unified",
|
|
39
|
-
"tool-calling",
|
|
40
|
-
"streaming",
|
|
41
|
-
"chatgpt",
|
|
42
|
-
"gpt",
|
|
43
|
-
"claude",
|
|
44
|
-
"mistral"
|
|
45
|
-
],
|
|
46
|
-
"author": "imenam",
|
|
47
|
-
"license": "MIT",
|
|
48
|
-
"publishConfig": {
|
|
49
|
-
"access": "public"
|
|
50
|
-
},
|
|
51
|
-
"repository": {
|
|
52
|
-
"type": "git",
|
|
53
|
-
"url": "https://github.com/imenam/globalLLMInterface.git"
|
|
54
|
-
},
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@types/mime-types": "^2.1.4",
|
|
57
|
-
"typescript": "^5.8.3"
|
|
58
|
-
},
|
|
59
|
-
"dependencies": {
|
|
60
|
-
"@anthropic-ai/sdk": "^0.52.0",
|
|
61
|
-
"@google/genai": "^
|
|
62
|
-
"@
|
|
63
|
-
"@types/node": "^
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@imenam/unified-llm-client",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "A unified interface for multiple LLM providers (OpenAI, Gemini, Anthropic, Qwen) with tool-calling, streaming, and audio support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc -w",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"release:patch": "npm version patch && npm publish",
|
|
17
|
+
"release:minor": "npm version minor && npm publish",
|
|
18
|
+
"release:major": "npm version major && npm publish",
|
|
19
|
+
"example": "node dist/examples/example.js",
|
|
20
|
+
"example:audio": "node dist/examples/audio-example.js",
|
|
21
|
+
"example:qwen": "node dist/examples/qwen-vl-example.js",
|
|
22
|
+
"example:qwen-omni": "node dist/examples/qwen-omni-audio-example.js",
|
|
23
|
+
"example:anthropic": "node dist/examples/anthropic-example.js",
|
|
24
|
+
"example:abort": "node dist/examples/chat-abort-example.js",
|
|
25
|
+
"example:proxy": "node dist/examples/proxy-example.js",
|
|
26
|
+
"example:proxy-api-key-resolver": "node dist/examples/proxy-api-key-resolver-example.js",
|
|
27
|
+
"example:api-key-resolver": "node dist/examples/api-key-resolver-example.js",
|
|
28
|
+
"proxy:start": "node dist/examples/proxy-server-start.js",
|
|
29
|
+
"example:error-logging": "node dist/examples/example-error-logging.js"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"llm",
|
|
33
|
+
"openai",
|
|
34
|
+
"gemini",
|
|
35
|
+
"anthropic",
|
|
36
|
+
"qwen",
|
|
37
|
+
"ai",
|
|
38
|
+
"unified",
|
|
39
|
+
"tool-calling",
|
|
40
|
+
"streaming",
|
|
41
|
+
"chatgpt",
|
|
42
|
+
"gpt",
|
|
43
|
+
"claude",
|
|
44
|
+
"mistral"
|
|
45
|
+
],
|
|
46
|
+
"author": "imenam",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/imenam/globalLLMInterface.git"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/mime-types": "^2.1.4",
|
|
57
|
+
"typescript": "^5.8.3"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
61
|
+
"@google/genai": "^2.31.0",
|
|
62
|
+
"@types/node": "^22.15.24",
|
|
63
|
+
"@types/node-fetch": "^2.6.13",
|
|
64
|
+
"dotenv": "^16.5.0",
|
|
65
|
+
"mime-types": "^3.0.1",
|
|
66
|
+
"node-fetch": "^3.3.2",
|
|
67
|
+
"sharp": "^0.35.5"
|
|
68
|
+
}
|
|
69
|
+
}
|