@imenam/unified-llm-client 2.0.0 → 2.0.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/README.md +30 -4
- package/dist/common/types.d.ts +1 -1
- package/dist/common/types.d.ts.map +1 -1
- package/dist/core/UnifiedLLMClient.js +1 -1
- package/dist/examples/api-key-resolver-example.js +2 -2
- package/dist/examples/proxy-api-key-resolver-example.js +2 -2
- package/dist/proxy/ProxyServer.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,32 @@ const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5, {
|
|
|
207
207
|
const response = await client.chat({ message: "Hello!" });
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
The proxy server also supports an `apiKeyResolver` for per-request dynamic key resolution (e.g. multi-tenant scenarios):
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { ProxyServer } from "@imenam/unified-llm-client";
|
|
214
|
+
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
|
|
215
|
+
|
|
216
|
+
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) =>
|
|
217
|
+
(await userKeyStore.get(ctx.userId)) ?? false;
|
|
218
|
+
|
|
219
|
+
const server = new ProxyServer({ port: 3000, apiKey: "my-secret", apiKeyResolver: resolver });
|
|
220
|
+
await server.start();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Client passes the context — server resolves the key
|
|
225
|
+
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
|
|
226
|
+
proxyUrl: "http://localhost:3000",
|
|
227
|
+
proxyApiKey: "my-secret",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const response = await client.chat({
|
|
231
|
+
message: "Hello!",
|
|
232
|
+
apiKeyContext: { userId: "user-123" },
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
210
236
|
## API Key Resolver
|
|
211
237
|
|
|
212
238
|
Dynamically resolve which API key to use for each request based on a custom context (e.g. per-user key, request metadata):
|
|
@@ -215,8 +241,8 @@ Dynamically resolve which API key to use for each request based on a custom cont
|
|
|
215
241
|
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
|
|
216
242
|
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
|
|
217
243
|
|
|
218
|
-
const resolver: ApiKeyResolver<{ userId: string }> = (ctx) => {
|
|
219
|
-
const key = userKeyStore.get(ctx.userId);
|
|
244
|
+
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) => {
|
|
245
|
+
const key = await userKeyStore.get(ctx.userId);
|
|
220
246
|
return key ?? false; // return false to deny access
|
|
221
247
|
};
|
|
222
248
|
|
|
@@ -239,7 +265,7 @@ UnifiedLLMClient.setDefaultApiKeyResolver((ctx) => {
|
|
|
239
265
|
```
|
|
240
266
|
|
|
241
267
|
> The resolver is called on every `chat()` call. Returning `false` throws an access-denied error.
|
|
242
|
-
> The `apiKeyResolver` option is
|
|
268
|
+
> 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.
|
|
243
269
|
|
|
244
270
|
## Cancellation
|
|
245
271
|
|
|
@@ -294,7 +320,7 @@ UnifiedLLMClient.setDefaultLogger((log) => {
|
|
|
294
320
|
| `options.proxyUrl` | `string` | Proxy server URL — activates proxy mode |
|
|
295
321
|
| `options.proxyApiKey` | `string` | Key sent in the `x-api-key` header (proxy mode) |
|
|
296
322
|
| `options.apiKey` | `string` | Static API key passed directly to the provider (overrides env var) |
|
|
297
|
-
| `options.apiKeyResolver` | `ApiKeyResolver` | Dynamic per-request key resolver (
|
|
323
|
+
| `options.apiKeyResolver` | `ApiKeyResolver` | Dynamic per-request key resolver (direct mode only — in proxy mode, set this on `ProxyServer`) |
|
|
298
324
|
|
|
299
325
|
### `client.chat(options)`
|
|
300
326
|
|
package/dist/common/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type Provider = "openai" | "gemini" | "qwen" | "anthropic";
|
|
2
|
-
export type ApiKeyResolver<T = unknown> = (context: T) => string | false;
|
|
2
|
+
export type ApiKeyResolver<T = unknown> = (context: T) => Promise<string | false> | string | false;
|
|
3
3
|
export type ChatMessageRole = "user" | "assistant" | "system" | "tool" | "function";
|
|
4
4
|
export interface ChatMessage {
|
|
5
5
|
tool_call_id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/common/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAElE,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/common/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -64,7 +64,7 @@ class UnifiedLLMClient {
|
|
|
64
64
|
if (apiKeyContext === undefined) {
|
|
65
65
|
throw new Error("[ApiKeyResolver] A resolver is defined but no apiKeyContext was provided.");
|
|
66
66
|
}
|
|
67
|
-
const resolvedKey = effectiveResolver(apiKeyContext);
|
|
67
|
+
const resolvedKey = yield effectiveResolver(apiKeyContext);
|
|
68
68
|
if (resolvedKey === false)
|
|
69
69
|
throw new Error("[ApiKeyResolver] The resolver returned false — access denied.");
|
|
70
70
|
const resolved = LLMAdapter_1.PROVIDERS_CONFIG[this.model.provider](resolvedKey);
|
|
@@ -22,7 +22,7 @@ const __1 = require("..");
|
|
|
22
22
|
* Exemple de resolver : renvoie la clé pour un tenant connu, sinon `false` (refus).
|
|
23
23
|
* En production, vous chargeriez la clé depuis une base, un vault, etc.
|
|
24
24
|
*/
|
|
25
|
-
const demoResolver = (ctx) => {
|
|
25
|
+
const demoResolver = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
26
|
const { tenantId } = ctx;
|
|
27
27
|
const byTenant = {
|
|
28
28
|
// Un seul slot de démo : même clé env que la config Gemini par défaut.
|
|
@@ -32,7 +32,7 @@ const demoResolver = (ctx) => {
|
|
|
32
32
|
if (!key)
|
|
33
33
|
return false;
|
|
34
34
|
return key;
|
|
35
|
-
};
|
|
35
|
+
});
|
|
36
36
|
function main() {
|
|
37
37
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
38
|
if (!process.env.GOOGLE_API_KEY) {
|
|
@@ -29,13 +29,13 @@ function main() {
|
|
|
29
29
|
const server = new __1.ProxyServer({
|
|
30
30
|
port: PROXY_PORT,
|
|
31
31
|
apiKey: PROXY_API_KEY,
|
|
32
|
-
apiKeyResolver: (ctx) => {
|
|
32
|
+
apiKeyResolver: (ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
33
33
|
const { tenantId } = ctx;
|
|
34
34
|
if (tenantId === "demo") {
|
|
35
35
|
return process.env.GOOGLE_API_KEY;
|
|
36
36
|
}
|
|
37
37
|
return false;
|
|
38
|
-
},
|
|
38
|
+
}),
|
|
39
39
|
});
|
|
40
40
|
yield server.start();
|
|
41
41
|
console.log(`[example] Proxy listening on http://localhost:${PROXY_PORT}\n`);
|
|
@@ -132,7 +132,7 @@ class ProxyServer {
|
|
|
132
132
|
res.end(JSON.stringify({ error: "apiKeyResolver is defined but no apiKeyContext was provided" }));
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
|
-
const result = this.options.apiKeyResolver(request.apiKeyContext);
|
|
135
|
+
const result = yield this.options.apiKeyResolver(request.apiKeyContext);
|
|
136
136
|
if (result === false) {
|
|
137
137
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
138
138
|
res.end(JSON.stringify({ error: "apiKeyResolver returned false" }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/unified-llm-client",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A unified interface for multiple LLM providers (OpenAI, Gemini, Anthropic, Qwen) with tool-calling, streaming, and audio support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|