@imenam/unified-llm-client
v2.1.0
Published
A unified interface for multiple LLM providers (OpenAI, Gemini, Anthropic, Qwen) with tool-calling, streaming, and audio support
Downloads
75
Maintainers
Readme
@imenam/unified-llm-client
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.
Features
- Unified API — one interface, four providers
- Tool-calling — automatic execution loop with structured results
- Streaming — chunk-by-chunk responses via callbacks
- Multimodal — image and audio file inputs
- Audio generation — Text-to-Speech (Gemini)
- Proxy mode — delegate LLM calls to a server, no API keys on the client
- API key resolver — dynamically resolve API keys per request from a user-provided context
- Cancellation — native
AbortSignalsupport - Usage tracking — token counts and estimated costs
- Full TypeScript support — exported types and
.d.tsdeclarations
Installation
npm install @imenam/unified-llm-clientSupported Providers
| Provider | Chat | Streaming | Tool-calling | Multimodal | Audio TTS | |-----------|:----:|:---------:|:------------:|:----------:|:---------:| | OpenAI | ✅ | ✅ | ✅ | ✅ | ❌ | | Gemini | ✅ | ✅ | ✅ | ✅ | ✅ | | Anthropic | ✅ | ✅ | ✅ | ✅ | ❌ | | Qwen | ✅ | ✅ | ✅ | ✅ | ❌ |
Quick Start
Set your API keys in a .env file:
OPENAI_API_KEY=sk-...
GOOGLE_API_KEY=...
ANTHROPIC_API_KEY=sk-ant-...
ALIBABA_CLOUD_API_KEY=... # for Qwenimport { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI);
const response = await client.chat({
message: "What is the capital of France?",
});
console.log(response.content); // "The capital of France is Paris."Available Models
import { MODELS } from "@imenam/unified-llm-client";
// OpenAI
MODELS.GPT_4O_MINI
MODELS.GPT_5
// Gemini
MODELS.GEMINI_FLASH_2_5
MODELS.GEMINI_2_5_PRO
MODELS.GEMINI_3_FLASH
MODELS.GEMINI_3_PRO
MODELS.GEMINI_3_1_PRO
// Anthropic
MODELS.CLAUDE_SONNET_4_6
MODELS.CLAUDE_OPUS_4_6
// Qwen
MODELS.QWEN_VL_3
MODELS.QWEN_VL_FLASH
MODELS.QWEN_VL_3_FLASHYou can also pass a custom model:
const client = new UnifiedLLMClient({
provider: "openai",
modelName: "gpt-4o",
costsPer1MTokens: { prompt: 2.5, output: 10 },
currency: "USD",
});Tool-calling
Define tools with an executor function — the client will call them automatically and loop until the LLM produces a final text response.
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
import type { ToolDefinition } from "@imenam/unified-llm-client";
const tools: ToolDefinition[] = [
{
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
},
required: ["city"],
},
executor: async ({ city }) => {
return `The weather in ${city} is sunny and 22°C.`;
},
},
];
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5);
const response = await client.chat({
message: "What's the weather like in Paris?",
tools,
});
console.log(response.content);Streaming
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI);
await client.chat({
message: "Tell me a short story.",
stream: true,
onChunk: (chunk) => {
process.stdout.write(chunk.contentChunk ?? "");
},
onFinalResponse: (final) => {
console.log("\n\nTotal tokens:", final.usage?.totalTokens);
},
});Multimodal Inputs
Attach images or audio files directly to a message:
const response = await client.chat({
message: {
role: "user",
content: "What do you see in this image?",
attachedFile: "./photo.jpg",
},
});Multiple files are also supported:
await client.chat({
message: {
role: "user",
content: "Compare these two images.",
attachedFile: ["./image1.jpg", "./image2.png"],
},
});Audio Generation (Gemini)
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5);
const audio = await client.generateAudio({
text: "Hello, this is a test of text-to-speech.",
outputPath: "./output.wav",
voiceConfig: { voiceName: "Kore" },
});Proxy Mode
Run an HTTP server that holds your API keys server-side, and connect clients without any keys:
import { ProxyServer } from "@imenam/unified-llm-client";
// Server side — API keys are read from process.env
const server = new ProxyServer({ port: 3000, apiKey: "my-secret" });
await server.start();// Client side — no API keys needed
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
const client = new UnifiedLLMClient(MODELS.GEMINI_FLASH_2_5, {
proxyUrl: "http://localhost:3000",
proxyApiKey: "my-secret",
});
// The chat() API is identical — streaming and file attachments are fully supported
const response = await client.chat({ message: "Hello!" });The proxy server also supports an apiKeyResolver for per-request dynamic key resolution (e.g. multi-tenant scenarios):
import { ProxyServer } from "@imenam/unified-llm-client";
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) =>
(await userKeyStore.get(ctx.userId)) ?? false;
const server = new ProxyServer({ port: 3000, apiKey: "my-secret", apiKeyResolver: resolver });
await server.start();// Client passes the context — server resolves the key
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
proxyUrl: "http://localhost:3000",
proxyApiKey: "my-secret",
});
const response = await client.chat({
message: "Hello!",
apiKeyContext: { userId: "user-123" },
});API Key Resolver
Dynamically resolve which API key to use for each request based on a custom context (e.g. per-user key, request metadata):
import { UnifiedLLMClient, MODELS } from "@imenam/unified-llm-client";
import type { ApiKeyResolver } from "@imenam/unified-llm-client";
const resolver: ApiKeyResolver<{ userId: string }> = async (ctx) => {
const key = await userKeyStore.get(ctx.userId);
return key ?? false; // return false to deny access
};
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
apiKeyResolver: resolver,
});
const response = await client.chat({
message: "Hello!",
apiKeyContext: { userId: "user-123" },
});You can also set a global default resolver for all instances:
UnifiedLLMClient.setDefaultApiKeyResolver((ctx) => {
return ctx.apiKey ?? false;
});The resolver is called on every
chat()call. Returningfalsethrows an access-denied error. The client-sideapiKeyResolveroption is for direct mode only. In proxy mode, the resolver is defined on theProxyServer— the client passesapiKeyContextand the server resolves the key.
Cancellation
Pass an AbortSignal to cancel a request mid-flight:
const ac = new AbortController();
setTimeout(() => ac.abort(), 800);
try {
await client.chat({
message: "Write a very long story...",
stream: true,
signal: ac.signal,
onChunk: (chunk) => process.stdout.write(chunk.contentChunk ?? ""),
});
} catch (e) {
if (e instanceof Error && e.name === "AbortError") {
console.log("Request cancelled.");
}
}Logging
Pass a logger callback to capture structured logs for every request:
const client = new UnifiedLLMClient(MODELS.GPT_4O_MINI, {
logger: (log) => {
console.log(`[${log.provider}] ${log.model} — ${log.usage.totalTokens} tokens`);
},
});Set a global default logger applied to all instances:
UnifiedLLMClient.setDefaultLogger((log) => {
console.log(`[${log.provider}] ${log.model} — ${log.usage.totalTokens} tokens`);
});API Reference
new UnifiedLLMClient(model, options?)
| Parameter | Type | Description |
|-----------|------|-------------|
| model | Model | Model object from MODELS or a custom { provider, modelName, costsPer1MTokens, currency } |
| options.logger | (log: UnifiedLog) => void | Optional logging callback |
| options.proxyUrl | string | Proxy server URL — activates proxy mode |
| options.proxyApiKey | string | Key sent in the x-api-key header (proxy mode) |
| options.apiKey | string | Static API key passed directly to the provider (overrides env var) |
| options.apiKeyResolver | ApiKeyResolver | Dynamic per-request key resolver (direct mode only — in proxy mode, set this on ProxyServer) |
client.chat(options)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| message | string \| ChatMessage \| ChatMessage[] | — | User message or full conversation history |
| tools | ToolDefinition[] | [] | Tools available to the LLM |
| toolChoice | "auto" \| "none" \| string | "auto" | Tool selection strategy |
| stream | boolean | false | Enable streaming |
| onChunk | (chunk: StreamedChatChunk) => void | — | Streaming chunk callback |
| onFinalResponse | (res: ChatResponse) => void | — | Final response callback |
| onLog | (log: UnifiedLog) => void | — | Per-call log callback |
| config | ChatOptionalConfig | — | Advanced options: thinkingBudget, metadata |
| signal | AbortSignal | — | Cancellation signal |
| apiKeyContext | unknown | — | Context passed to apiKeyResolver to resolve the API key |
ChatResponse
interface ChatResponse {
content: string | null;
toolCalls?: ToolCall[];
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
finishReason?: string;
requestId?: string;
raw: unknown;
}License
MIT
