@im-bot/llm-client 0.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 +194 -0
- package/dist/async-client.d.ts +20 -0
- package/dist/async-client.js +29 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +385 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +98 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +44 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/messages.d.ts +15 -0
- package/dist/messages.js +38 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +18 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +3 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @im-bot/llm-client (TypeScript)
|
|
2
|
+
|
|
3
|
+
Unified multi-provider LLM client — TypeScript port of the
|
|
4
|
+
Python [`im_bot_llm`](../../python) SDK. Same feature set:
|
|
5
|
+
|
|
6
|
+
- Multi-provider with automatic fallback chain
|
|
7
|
+
- Retry with exponential backoff on 429 / 5xx
|
|
8
|
+
- Streaming via async iterables (SSE)
|
|
9
|
+
- Tool use / function calling loop
|
|
10
|
+
- Token counting + cost estimation (USD)
|
|
11
|
+
- TypeScript-native: full type definitions, ESM output, zero runtime deps
|
|
12
|
+
(uses Node 18+ global `fetch`)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# from the im-bot repo (development):
|
|
18
|
+
pnpm install -w packages/llm-client/typescript
|
|
19
|
+
# or in a standalone project:
|
|
20
|
+
npm install @im-bot/llm-client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Simple chat
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { LLMClient, Provider, Message } from '@im-bot/llm-client';
|
|
29
|
+
|
|
30
|
+
const client = new LLMClient({
|
|
31
|
+
defaultProvider: 'deepseek',
|
|
32
|
+
providers: {
|
|
33
|
+
deepseek: new Provider({ apiKey: process.env.DEEPSEEK_API_KEY! }),
|
|
34
|
+
openrouter: new Provider({ apiKey: process.env.OPENROUTER_API_KEY! }),
|
|
35
|
+
},
|
|
36
|
+
fallbackOrder: ['deepseek', 'openrouter'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const reply = await client.chat(
|
|
40
|
+
[Message.user('Say hello in 5 words.')],
|
|
41
|
+
{ model: 'deepseek-flash' },
|
|
42
|
+
);
|
|
43
|
+
console.log(reply.text);
|
|
44
|
+
console.log(`cost: $${reply.costUsd.toFixed(5)}`);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Streaming
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
for await (const chunk of client.streamChat(
|
|
51
|
+
[Message.user('Write a short poem.')],
|
|
52
|
+
{ model: 'deepseek-flash' },
|
|
53
|
+
)) {
|
|
54
|
+
if (chunk.delta) process.stdout.write(chunk.delta);
|
|
55
|
+
if (chunk.isFinal) console.log(`\n[tokens: ${chunk.usage.totalTokens}]`);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Tool use
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { LLMClient, Provider, Message, ToolSpec } from '@im-bot/llm-client';
|
|
63
|
+
|
|
64
|
+
const searchTool = new ToolSpec({
|
|
65
|
+
name: 'search',
|
|
66
|
+
description: 'Search the web.',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: { query: { type: 'string' } },
|
|
70
|
+
required: ['query'],
|
|
71
|
+
},
|
|
72
|
+
handler: ({ query }: { query: string }) => `Results for ${query}`,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const client = new LLMClient({
|
|
76
|
+
defaultProvider: 'deepseek',
|
|
77
|
+
providers: { deepseek: new Provider({ apiKey: process.env.DEEPSEEK_API_KEY! }) },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const reply = await client.chatWithTools(
|
|
81
|
+
[Message.user('find latest Python 3.13 release')],
|
|
82
|
+
{ tools: [searchTool], maxToolIterations: 3, model: 'deepseek-flash' },
|
|
83
|
+
);
|
|
84
|
+
console.log(reply.text);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Multi-provider fallback
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const client = new LLMClient({
|
|
91
|
+
defaultProvider: 'deepseek',
|
|
92
|
+
providers: {
|
|
93
|
+
deepseek: new Provider({ apiKey: 'sk-deepseek-...' }),
|
|
94
|
+
openrouter: new Provider({ apiKey: 'sk-or-...' }),
|
|
95
|
+
},
|
|
96
|
+
fallbackOrder: ['deepseek', 'openrouter'],
|
|
97
|
+
retry: { maxAttempts: 3, backoffFactor: 0.5, jitter: true },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// If deepseek is down or returns 429/5xx, automatically retries on openrouter.
|
|
101
|
+
const reply = await client.chat([Message.user('hi')], { model: 'deepseek-flash' });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API surface
|
|
105
|
+
|
|
106
|
+
### `LLMClient`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
class LLMClient {
|
|
110
|
+
constructor(config: {
|
|
111
|
+
defaultProvider: string;
|
|
112
|
+
providers: Record<string, Provider>;
|
|
113
|
+
fallbackOrder?: string[];
|
|
114
|
+
retry?: RetryConfig;
|
|
115
|
+
timeout?: number;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
chat(
|
|
119
|
+
messages: Message[],
|
|
120
|
+
options?: {
|
|
121
|
+
model?: string;
|
|
122
|
+
provider?: string;
|
|
123
|
+
temperature?: number;
|
|
124
|
+
maxTokens?: number;
|
|
125
|
+
tools?: ToolSpec[];
|
|
126
|
+
},
|
|
127
|
+
): Promise<ChatResponse>;
|
|
128
|
+
|
|
129
|
+
streamChat(
|
|
130
|
+
messages: Message[],
|
|
131
|
+
options?: { model?: string; provider?: string; temperature?: number; maxTokens?: number; },
|
|
132
|
+
): AsyncIterable<StreamChunk>;
|
|
133
|
+
|
|
134
|
+
chatWithTools(
|
|
135
|
+
messages: Message[],
|
|
136
|
+
options: { tools: ToolSpec[]; maxToolIterations?: number; model?: string; provider?: string },
|
|
137
|
+
): Promise<ChatResponse>;
|
|
138
|
+
|
|
139
|
+
close(): Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `Message`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
class Message {
|
|
147
|
+
constructor(
|
|
148
|
+
public role: 'system' | 'user' | 'assistant' | 'tool',
|
|
149
|
+
public content: string,
|
|
150
|
+
public name?: string,
|
|
151
|
+
public toolCallId?: string,
|
|
152
|
+
public toolCalls?: ToolCall[],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
static system(content: string): Message;
|
|
156
|
+
static user(content: string): Message;
|
|
157
|
+
static assistant(content: string, toolCalls?: ToolCall[]): Message;
|
|
158
|
+
static toolResult(toolCallId: string, name: string, content: string): Message;
|
|
159
|
+
|
|
160
|
+
toOpenAIDict(): Record<string, unknown>;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `ChatResponse`
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface ChatResponse {
|
|
168
|
+
text: string;
|
|
169
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
|
|
170
|
+
usage: Usage;
|
|
171
|
+
costUsd: number;
|
|
172
|
+
model: string;
|
|
173
|
+
provider: string;
|
|
174
|
+
raw?: unknown;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Environment-variable convention
|
|
179
|
+
|
|
180
|
+
LLMClient auto-detects keys via standard names when `Provider({ apiKey })` is
|
|
181
|
+
left blank:
|
|
182
|
+
|
|
183
|
+
| Provider | Env var |
|
|
184
|
+
|-----------|------------------------|
|
|
185
|
+
| deepseek | DEEPSEEK_API_KEY |
|
|
186
|
+
| openrouter| OPENROUTER_API_KEY |
|
|
187
|
+
| openai | OPENAI_API_KEY |
|
|
188
|
+
| minimax | MINIMAX_API_KEY |
|
|
189
|
+
| minimax_cn| MINIMAX_CN_API_KEY |
|
|
190
|
+
| zhipu | GLM_API_KEY |
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT — same as im-bot.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LLMClientConfig } from './client.js';
|
|
2
|
+
import { Message } from './messages.js';
|
|
3
|
+
import type { ChatResponse, StreamChunk, ChatOptions, StreamOptions, ToolChatOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* AsyncLLMClient — thin async-only wrapper around LLMClient.
|
|
6
|
+
*
|
|
7
|
+
* Node.js has no real async I/O advantage for HTTP (global fetch is already
|
|
8
|
+
* promise-based), so this class simply re-exports the sync API as async-only.
|
|
9
|
+
* Use this when you want a clean async surface and don't need the streaming
|
|
10
|
+
* generator pattern.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AsyncLLMClient {
|
|
13
|
+
private client;
|
|
14
|
+
constructor(config: LLMClientConfig);
|
|
15
|
+
chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse>;
|
|
16
|
+
streamChat(messages: Message[], options?: StreamOptions): AsyncIterable<StreamChunk>;
|
|
17
|
+
chatWithTools(messages: Message[], options: ToolChatOptions): Promise<ChatResponse>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=async-client.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @im-bot/llm-client — async LLMClient (delegates to sync via to_thread-like approach)
|
|
2
|
+
import { LLMClient } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* AsyncLLMClient — thin async-only wrapper around LLMClient.
|
|
5
|
+
*
|
|
6
|
+
* Node.js has no real async I/O advantage for HTTP (global fetch is already
|
|
7
|
+
* promise-based), so this class simply re-exports the sync API as async-only.
|
|
8
|
+
* Use this when you want a clean async surface and don't need the streaming
|
|
9
|
+
* generator pattern.
|
|
10
|
+
*/
|
|
11
|
+
export class AsyncLLMClient {
|
|
12
|
+
client;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.client = new LLMClient(config);
|
|
15
|
+
}
|
|
16
|
+
async chat(messages, options) {
|
|
17
|
+
return this.client.chat(messages, options);
|
|
18
|
+
}
|
|
19
|
+
streamChat(messages, options) {
|
|
20
|
+
return this.client.streamChat(messages, options);
|
|
21
|
+
}
|
|
22
|
+
async chatWithTools(messages, options) {
|
|
23
|
+
return this.client.chatWithTools(messages, options);
|
|
24
|
+
}
|
|
25
|
+
async close() {
|
|
26
|
+
return this.client.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=async-client.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Message } from './messages.js';
|
|
2
|
+
import { Provider, RetryConfigOptions } from './config.js';
|
|
3
|
+
import type { ChatResponse, StreamChunk, ChatOptions, StreamOptions, ToolChatOptions } from './types.js';
|
|
4
|
+
export interface LLMClientConfig {
|
|
5
|
+
defaultProvider: string;
|
|
6
|
+
providers: Record<string, Provider>;
|
|
7
|
+
fallbackOrder?: string[];
|
|
8
|
+
retry?: RetryConfigOptions;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class LLMClient {
|
|
12
|
+
private defaultProvider;
|
|
13
|
+
private providers;
|
|
14
|
+
private fallbackOrder;
|
|
15
|
+
private retry;
|
|
16
|
+
private timeout;
|
|
17
|
+
constructor(config: LLMClientConfig);
|
|
18
|
+
chat(messages: Message[], options?: ChatOptions): Promise<ChatResponse>;
|
|
19
|
+
streamChat(messages: Message[], options?: StreamOptions): AsyncIterable<StreamChunk>;
|
|
20
|
+
chatWithTools(messages: Message[], options: ToolChatOptions): Promise<ChatResponse>;
|
|
21
|
+
private resolveChain;
|
|
22
|
+
private defaultModelFor;
|
|
23
|
+
private url;
|
|
24
|
+
private headers;
|
|
25
|
+
private provTimeout;
|
|
26
|
+
private postWithRetry;
|
|
27
|
+
private streamWithRetry;
|
|
28
|
+
private extractToolCalls;
|
|
29
|
+
private runTools;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// @im-bot/llm-client — sync LLMClient
|
|
2
|
+
import { Message } from './messages.js';
|
|
3
|
+
import { RetryConfig } from './config.js';
|
|
4
|
+
import { LLMError, ProviderError, RateLimitError, AllProvidersFailedError, StreamInterruptedError, } from './errors.js';
|
|
5
|
+
// ── helpers ────────────────────────────────────────────────────────────
|
|
6
|
+
function defaultTimeout() {
|
|
7
|
+
return 60;
|
|
8
|
+
}
|
|
9
|
+
function shouldRetry(status, retry) {
|
|
10
|
+
return retry.shouldRetry(status);
|
|
11
|
+
}
|
|
12
|
+
function estimateCost(model, usage) {
|
|
13
|
+
// Mirror the Python SDK's pricing map (config.ts).
|
|
14
|
+
const pricing = {
|
|
15
|
+
'deepseek-flash': [0.07, 0.27],
|
|
16
|
+
'deepseek-v4-flash': [0.07, 0.27],
|
|
17
|
+
'deepseek-chat': [0.27, 1.10],
|
|
18
|
+
'minimax/MiniMax-M3': [1.0, 3.0],
|
|
19
|
+
'minimax/MiniMax-M2': [1.0, 3.0],
|
|
20
|
+
};
|
|
21
|
+
const p = pricing[model];
|
|
22
|
+
if (!p || usage.totalTokens === 0)
|
|
23
|
+
return 0;
|
|
24
|
+
return (usage.inputTokens / 1_000_000) * p[0] + (usage.outputTokens / 1_000_000) * p[1];
|
|
25
|
+
}
|
|
26
|
+
function buildRequestBody(opts) {
|
|
27
|
+
const body = {
|
|
28
|
+
model: opts.model,
|
|
29
|
+
messages: opts.messages.map((m) => m.toOpenAIDict()),
|
|
30
|
+
temperature: opts.temperature,
|
|
31
|
+
stream: opts.stream,
|
|
32
|
+
};
|
|
33
|
+
if (opts.maxTokens !== undefined)
|
|
34
|
+
body.max_tokens = opts.maxTokens;
|
|
35
|
+
if (opts.tools && opts.tools.length > 0) {
|
|
36
|
+
body.tools = opts.tools.map((t) => t.toOpenAIDict());
|
|
37
|
+
}
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
async function delay(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
function httpErrorToException(provider, status, body) {
|
|
44
|
+
if (status === 429) {
|
|
45
|
+
return new RateLimitError(provider, status, 'rate limited', body);
|
|
46
|
+
}
|
|
47
|
+
return new ProviderError(provider, status, `HTTP ${status}`, body);
|
|
48
|
+
}
|
|
49
|
+
function parseUsage(raw) {
|
|
50
|
+
if (!raw)
|
|
51
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
52
|
+
return {
|
|
53
|
+
inputTokens: Number(raw.prompt_tokens ?? 0),
|
|
54
|
+
outputTokens: Number(raw.completion_tokens ?? 0),
|
|
55
|
+
totalTokens: Number(raw.total_tokens ?? 0),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function parseChatResponse(provider, data) {
|
|
59
|
+
const choice = (data.choices ?? [])[0] ?? {};
|
|
60
|
+
const message = choice.message ?? {};
|
|
61
|
+
const text = message.content ?? '';
|
|
62
|
+
const finishReason = choice.finish_reason ?? 'stop';
|
|
63
|
+
const usage = parseUsage(data.usage);
|
|
64
|
+
const model = data.model ?? '';
|
|
65
|
+
const costUsd = estimateCost(model, usage);
|
|
66
|
+
return { text, finishReason, usage, costUsd, model, provider, raw: data };
|
|
67
|
+
}
|
|
68
|
+
// ── LLMClient ─────────────────────────────────────────────────────────
|
|
69
|
+
export class LLMClient {
|
|
70
|
+
defaultProvider;
|
|
71
|
+
providers;
|
|
72
|
+
fallbackOrder;
|
|
73
|
+
retry;
|
|
74
|
+
timeout;
|
|
75
|
+
constructor(config) {
|
|
76
|
+
this.defaultProvider = config.defaultProvider;
|
|
77
|
+
this.providers = config.providers;
|
|
78
|
+
this.fallbackOrder = config.fallbackOrder ?? [config.defaultProvider];
|
|
79
|
+
this.retry = RetryConfig.fromOptions(config.retry);
|
|
80
|
+
this.timeout = config.timeout ?? defaultTimeout();
|
|
81
|
+
}
|
|
82
|
+
// ── chat (non-streaming) ──────────────────────────────────────────
|
|
83
|
+
async chat(messages, options = {}) {
|
|
84
|
+
const chain = this.resolveChain(options.provider);
|
|
85
|
+
const effectiveModel = options.model ?? this.defaultModelFor(chain[0]);
|
|
86
|
+
const body = buildRequestBody({
|
|
87
|
+
messages,
|
|
88
|
+
model: effectiveModel,
|
|
89
|
+
temperature: options.temperature ?? 0.7,
|
|
90
|
+
maxTokens: options.maxTokens,
|
|
91
|
+
tools: options.tools,
|
|
92
|
+
stream: false,
|
|
93
|
+
});
|
|
94
|
+
const attempts = [];
|
|
95
|
+
for (let i = 0; i < chain.length; i++) {
|
|
96
|
+
const provName = chain[i];
|
|
97
|
+
try {
|
|
98
|
+
return await this.postWithRetry(provName, body);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof RateLimitError || err instanceof ProviderError) {
|
|
102
|
+
attempts.push({ provider: provName, error: err });
|
|
103
|
+
// Only swallow provider-level errors if there's another to try.
|
|
104
|
+
if (i + 1 >= chain.length)
|
|
105
|
+
throw err;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new AllProvidersFailedError(attempts);
|
|
112
|
+
}
|
|
113
|
+
// ── streamChat ────────────────────────────────────────────────────
|
|
114
|
+
async *streamChat(messages, options = {}) {
|
|
115
|
+
const chain = this.resolveChain(options.provider);
|
|
116
|
+
const effectiveModel = options.model ?? this.defaultModelFor(chain[0]);
|
|
117
|
+
const body = buildRequestBody({
|
|
118
|
+
messages,
|
|
119
|
+
model: effectiveModel,
|
|
120
|
+
temperature: options.temperature ?? 0.7,
|
|
121
|
+
maxTokens: options.maxTokens,
|
|
122
|
+
stream: true,
|
|
123
|
+
});
|
|
124
|
+
let lastErr = null;
|
|
125
|
+
for (let i = 0; i < chain.length; i++) {
|
|
126
|
+
const provName = chain[i];
|
|
127
|
+
try {
|
|
128
|
+
for await (const chunk of this.streamWithRetry(provName, body)) {
|
|
129
|
+
yield chunk;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof RateLimitError ||
|
|
135
|
+
err instanceof ProviderError ||
|
|
136
|
+
err instanceof StreamInterruptedError) {
|
|
137
|
+
lastErr = err;
|
|
138
|
+
if (i + 1 >= chain.length)
|
|
139
|
+
throw err;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new AllProvidersFailedError(chain.map((p) => ({ provider: p, error: lastErr ?? new Error('unknown') })));
|
|
146
|
+
}
|
|
147
|
+
// ── chatWithTools ───────────────────────────────────────────────
|
|
148
|
+
async chatWithTools(messages, options) {
|
|
149
|
+
const { tools, maxToolIterations = 5, ...chatOpts } = options;
|
|
150
|
+
let currentMessages = [...messages];
|
|
151
|
+
let cumulative = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
152
|
+
let last = null;
|
|
153
|
+
for (let i = 0; i < maxToolIterations; i++) {
|
|
154
|
+
const response = await this.chat(currentMessages, { ...chatOpts, tools });
|
|
155
|
+
cumulative = {
|
|
156
|
+
inputTokens: cumulative.inputTokens + response.usage.inputTokens,
|
|
157
|
+
outputTokens: cumulative.outputTokens + response.usage.outputTokens,
|
|
158
|
+
totalTokens: cumulative.totalTokens + response.usage.totalTokens,
|
|
159
|
+
};
|
|
160
|
+
last = response;
|
|
161
|
+
if (response.finishReason !== 'tool_calls')
|
|
162
|
+
break;
|
|
163
|
+
const toolCalls = this.extractToolCalls(response);
|
|
164
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
165
|
+
break;
|
|
166
|
+
currentMessages.push(Message.assistant(response.text || '', toolCalls));
|
|
167
|
+
const results = await this.runTools(toolCalls, tools);
|
|
168
|
+
for (const r of results) {
|
|
169
|
+
currentMessages.push(Message.toolResult(r.toolCallId, r.name, r.output));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!last)
|
|
173
|
+
throw new LLMError('chatWithTools: no response produced');
|
|
174
|
+
return {
|
|
175
|
+
...last,
|
|
176
|
+
usage: cumulative,
|
|
177
|
+
costUsd: estimateCost(last.model, cumulative),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ── internal ─────────────────────────────────────────────────────
|
|
181
|
+
resolveChain(override) {
|
|
182
|
+
if (override) {
|
|
183
|
+
if (!(override in this.providers)) {
|
|
184
|
+
throw new Error(`Unknown provider: ${override}`);
|
|
185
|
+
}
|
|
186
|
+
return [override];
|
|
187
|
+
}
|
|
188
|
+
return [...this.fallbackOrder];
|
|
189
|
+
}
|
|
190
|
+
defaultModelFor(provName) {
|
|
191
|
+
const p = this.providers[provName];
|
|
192
|
+
return p ? p.defaultModel() : '';
|
|
193
|
+
}
|
|
194
|
+
url(provName) {
|
|
195
|
+
const p = this.providers[provName];
|
|
196
|
+
if (!p)
|
|
197
|
+
throw new Error(`Unknown provider: ${provName}`);
|
|
198
|
+
return `${p.resolveBaseUrl(provName).replace(/\/$/, '')}/chat/completions`;
|
|
199
|
+
}
|
|
200
|
+
headers(provName) {
|
|
201
|
+
const p = this.providers[provName];
|
|
202
|
+
if (!p)
|
|
203
|
+
throw new Error(`Unknown provider: ${provName}`);
|
|
204
|
+
return {
|
|
205
|
+
Authorization: `Bearer ${p.resolveApiKey(provName)}`,
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
...p.extraHeaders(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
provTimeout(provName) {
|
|
211
|
+
const p = this.providers[provName];
|
|
212
|
+
return p ? p.timeout(this.timeout) : this.timeout;
|
|
213
|
+
}
|
|
214
|
+
async postWithRetry(provName, body) {
|
|
215
|
+
let lastErr = null;
|
|
216
|
+
for (let attempt = 0; attempt < this.retry.maxAttempts; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const resp = await fetch(this.url(provName), {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: this.headers(provName),
|
|
221
|
+
body: JSON.stringify(body),
|
|
222
|
+
signal: AbortSignal.timeout(this.provTimeout(provName) * 1000),
|
|
223
|
+
});
|
|
224
|
+
if (resp.ok) {
|
|
225
|
+
const data = await resp.json();
|
|
226
|
+
return parseChatResponse(provName, data);
|
|
227
|
+
}
|
|
228
|
+
const text = await resp.text().catch(() => '');
|
|
229
|
+
const err = httpErrorToException(provName, resp.status, text.slice(0, 500));
|
|
230
|
+
lastErr = err;
|
|
231
|
+
if (shouldRetry(resp.status, this.retry) && attempt + 1 < this.retry.maxAttempts) {
|
|
232
|
+
await this.retry.delay(attempt);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (err instanceof ProviderError) {
|
|
239
|
+
lastErr = err;
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
// Network/abort errors
|
|
243
|
+
const wrapped = new ProviderError(provName, null, String(err.message ?? err));
|
|
244
|
+
lastErr = wrapped;
|
|
245
|
+
if (attempt + 1 < this.retry.maxAttempts) {
|
|
246
|
+
await this.retry.delay(attempt);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
throw wrapped;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw lastErr ?? new ProviderError(provName, null, 'no attempts made');
|
|
253
|
+
}
|
|
254
|
+
async *streamWithRetry(provName, body) {
|
|
255
|
+
let lastErr = null;
|
|
256
|
+
for (let attempt = 0; attempt < this.retry.maxAttempts; attempt++) {
|
|
257
|
+
try {
|
|
258
|
+
const resp = await fetch(this.url(provName), {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: this.headers(provName),
|
|
261
|
+
body: JSON.stringify(body),
|
|
262
|
+
signal: AbortSignal.timeout(this.provTimeout(provName) * 1000),
|
|
263
|
+
});
|
|
264
|
+
if (!resp.ok) {
|
|
265
|
+
const text = await resp.text().catch(() => '');
|
|
266
|
+
const err = httpErrorToException(provName, resp.status, text.slice(0, 500));
|
|
267
|
+
lastErr = err;
|
|
268
|
+
if (shouldRetry(resp.status, this.retry) && attempt + 1 < this.retry.maxAttempts) {
|
|
269
|
+
await this.retry.delay(attempt);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
const reader = resp.body?.getReader();
|
|
275
|
+
if (!reader)
|
|
276
|
+
throw new StreamInterruptedError('no response body');
|
|
277
|
+
const decoder = new TextDecoder();
|
|
278
|
+
let buffer = '';
|
|
279
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
280
|
+
let finalReason = '';
|
|
281
|
+
let finalModel = String(body.model || '');
|
|
282
|
+
while (true) {
|
|
283
|
+
const { value, done } = await reader.read();
|
|
284
|
+
if (done)
|
|
285
|
+
break;
|
|
286
|
+
buffer += decoder.decode(value, { stream: true });
|
|
287
|
+
let nlIdx;
|
|
288
|
+
while ((nlIdx = buffer.indexOf('\n')) !== -1) {
|
|
289
|
+
const line = buffer.slice(0, nlIdx).trim();
|
|
290
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
291
|
+
if (!line.startsWith('data:'))
|
|
292
|
+
continue;
|
|
293
|
+
const payload = line.slice(5).trim();
|
|
294
|
+
if (payload === '[DONE]')
|
|
295
|
+
continue;
|
|
296
|
+
let evt;
|
|
297
|
+
try {
|
|
298
|
+
evt = JSON.parse(payload);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let deltaText = '';
|
|
304
|
+
for (const ch of evt.choices ?? []) {
|
|
305
|
+
deltaText += (ch.delta?.content ?? '');
|
|
306
|
+
}
|
|
307
|
+
if (deltaText) {
|
|
308
|
+
yield { delta: deltaText, isFinal: false, usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, finishReason: '' };
|
|
309
|
+
}
|
|
310
|
+
if (evt.usage)
|
|
311
|
+
finalUsage = parseUsage(evt.usage);
|
|
312
|
+
for (const ch of evt.choices ?? []) {
|
|
313
|
+
if (ch.finish_reason)
|
|
314
|
+
finalReason = ch.finish_reason;
|
|
315
|
+
if (ch.model)
|
|
316
|
+
finalModel = ch.model;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
yield {
|
|
321
|
+
delta: '',
|
|
322
|
+
isFinal: true,
|
|
323
|
+
usage: finalUsage,
|
|
324
|
+
finishReason: finalReason || 'stop',
|
|
325
|
+
};
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
if (err instanceof ProviderError || err instanceof RateLimitError) {
|
|
330
|
+
lastErr = err;
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
const wrapped = new StreamInterruptedError(String(err.message ?? err));
|
|
334
|
+
lastErr = wrapped;
|
|
335
|
+
if (attempt + 1 < this.retry.maxAttempts) {
|
|
336
|
+
await this.retry.delay(attempt);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
throw wrapped;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
throw lastErr ?? new StreamInterruptedError('no attempts made');
|
|
343
|
+
}
|
|
344
|
+
extractToolCalls(response) {
|
|
345
|
+
const data = response.raw;
|
|
346
|
+
if (!data)
|
|
347
|
+
return null;
|
|
348
|
+
const choice = (data.choices ?? [])[0];
|
|
349
|
+
if (!choice)
|
|
350
|
+
return null;
|
|
351
|
+
return choice.message?.tool_calls ?? null;
|
|
352
|
+
}
|
|
353
|
+
async runTools(calls, specs) {
|
|
354
|
+
const byName = new Map(specs.map((s) => [s.config.name, s]));
|
|
355
|
+
const results = [];
|
|
356
|
+
for (const call of calls) {
|
|
357
|
+
const fn = call.function ?? {};
|
|
358
|
+
const name = fn.name;
|
|
359
|
+
let parsed = {};
|
|
360
|
+
try {
|
|
361
|
+
parsed = JSON.parse(fn.arguments ?? '{}');
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
parsed = {};
|
|
365
|
+
}
|
|
366
|
+
const spec = byName.get(name);
|
|
367
|
+
if (!spec) {
|
|
368
|
+
results.push({ toolCallId: call.id, name, output: `Error: unknown tool '${name}'` });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const out = await Promise.resolve(spec.config.handler(parsed));
|
|
373
|
+
results.push({ toolCallId: call.id, name, output: String(out) });
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
results.push({ toolCallId: call.id, name, output: `Error: ${String(err)}` });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return results;
|
|
380
|
+
}
|
|
381
|
+
async close() {
|
|
382
|
+
// No persistent connections in Node global fetch — nothing to close.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
//# sourceMappingURL=client.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type ProviderEnvVar = 'deepseek' | 'openrouter' | 'openai' | 'minimax' | 'minimax_cn' | 'zhipu' | 'kimi' | 'anthropic';
|
|
2
|
+
export declare const PROVIDER_ENV_VARS: Record<ProviderEnvVar, string>;
|
|
3
|
+
export declare const PROVIDER_BASE_URLS: Record<ProviderEnvVar, string>;
|
|
4
|
+
export declare const MODEL_PRICING: Record<string, [number, number]>;
|
|
5
|
+
export interface ProviderConfig {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export declare class Provider {
|
|
14
|
+
config: ProviderConfig;
|
|
15
|
+
constructor(config?: ProviderConfig);
|
|
16
|
+
/** The display name for this provider (defaults to the key in `LLMClient.providers`). */
|
|
17
|
+
displayName(fallback: string): string;
|
|
18
|
+
/** Resolve the base URL (Provider override → PROVIDER_BASE_URLS default). */
|
|
19
|
+
resolveBaseUrl(name: string): string;
|
|
20
|
+
/** Resolve the API key (Provider.apiKey → env var). */
|
|
21
|
+
resolveApiKey(name: string): string;
|
|
22
|
+
/** Default model (or empty string if unset). */
|
|
23
|
+
defaultModel(): string;
|
|
24
|
+
/** Per-request timeout in seconds. */
|
|
25
|
+
timeout(defaultTimeout: number): number;
|
|
26
|
+
/** Extra HTTP headers for every request. */
|
|
27
|
+
extraHeaders(): Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
export interface RetryConfigOptions {
|
|
30
|
+
maxAttempts?: number;
|
|
31
|
+
backoffFactor?: number;
|
|
32
|
+
jitter?: boolean;
|
|
33
|
+
retryOnStatus?: number[];
|
|
34
|
+
}
|
|
35
|
+
export declare class RetryConfig {
|
|
36
|
+
maxAttempts: number;
|
|
37
|
+
backoffFactor: number;
|
|
38
|
+
jitter: boolean;
|
|
39
|
+
retryOnStatus: readonly number[];
|
|
40
|
+
constructor(maxAttempts?: number, backoffFactor?: number, jitter?: boolean, retryOnStatus?: readonly number[]);
|
|
41
|
+
static fromOptions(opts?: RetryConfigOptions): RetryConfig;
|
|
42
|
+
shouldRetry(status: number): boolean;
|
|
43
|
+
sleepForBackoff(attempt: number): number;
|
|
44
|
+
delay(attempt: number): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @im-bot/llm-client — Provider + RetryConfig
|
|
2
|
+
// Standard env var per provider (for auto-detect when apiKey is omitted)
|
|
3
|
+
export const PROVIDER_ENV_VARS = {
|
|
4
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
5
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
6
|
+
openai: 'OPENAI_API_KEY',
|
|
7
|
+
minimax: 'MINIMAX_API_KEY',
|
|
8
|
+
minimax_cn: 'MINIMAX_CN_API_KEY',
|
|
9
|
+
zhipu: 'GLM_API_KEY',
|
|
10
|
+
kimi: 'KIMI_API_KEY',
|
|
11
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
12
|
+
};
|
|
13
|
+
// Default base_url per provider (override per-Provider if needed)
|
|
14
|
+
export const PROVIDER_BASE_URLS = {
|
|
15
|
+
deepseek: 'https://api.deepseek.com/v1',
|
|
16
|
+
openrouter: 'https://openrouter.ai/api/v1',
|
|
17
|
+
openai: 'https://api.openai.com/v1',
|
|
18
|
+
minimax: 'https://api.minimax.chat/v1',
|
|
19
|
+
minimax_cn: 'https://api.minimaxi.com/anthropic',
|
|
20
|
+
zhipu: 'https://open.bigmodel.cn/api/paas/v4/',
|
|
21
|
+
kimi: 'https://api.moonshot.cn/v1',
|
|
22
|
+
anthropic: 'https://api.anthropic.com/v1',
|
|
23
|
+
};
|
|
24
|
+
// Approximate cost per 1M tokens (input, output). Conservative defaults.
|
|
25
|
+
export const MODEL_PRICING = {
|
|
26
|
+
'deepseek-flash': [0.07, 0.27],
|
|
27
|
+
'deepseek-v4-flash': [0.07, 0.27],
|
|
28
|
+
'deepseek-chat': [0.27, 1.10],
|
|
29
|
+
'minimax/MiniMax-M3': [1.0, 3.0],
|
|
30
|
+
'minimax/MiniMax-M2': [1.0, 3.0],
|
|
31
|
+
};
|
|
32
|
+
export class Provider {
|
|
33
|
+
config;
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
}
|
|
37
|
+
/** The display name for this provider (defaults to the key in `LLMClient.providers`). */
|
|
38
|
+
displayName(fallback) {
|
|
39
|
+
return this.config.name || fallback;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve the base URL (Provider override → PROVIDER_BASE_URLS default). */
|
|
42
|
+
resolveBaseUrl(name) {
|
|
43
|
+
if (this.config.baseUrl)
|
|
44
|
+
return this.config.baseUrl;
|
|
45
|
+
if (name in PROVIDER_BASE_URLS)
|
|
46
|
+
return PROVIDER_BASE_URLS[name];
|
|
47
|
+
throw new Error(`Unknown provider '${name}' and no baseUrl configured`);
|
|
48
|
+
}
|
|
49
|
+
/** Resolve the API key (Provider.apiKey → env var). */
|
|
50
|
+
resolveApiKey(name) {
|
|
51
|
+
if (this.config.apiKey)
|
|
52
|
+
return this.config.apiKey;
|
|
53
|
+
const envVar = PROVIDER_ENV_VARS[name];
|
|
54
|
+
if (envVar && process.env[envVar]) {
|
|
55
|
+
return process.env[envVar];
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`No API key for provider '${name}': set Provider.apiKey or env var ${envVar}`);
|
|
58
|
+
}
|
|
59
|
+
/** Default model (or empty string if unset). */
|
|
60
|
+
defaultModel() {
|
|
61
|
+
return this.config.model || '';
|
|
62
|
+
}
|
|
63
|
+
/** Per-request timeout in seconds. */
|
|
64
|
+
timeout(defaultTimeout) {
|
|
65
|
+
return this.config.timeout ?? defaultTimeout;
|
|
66
|
+
}
|
|
67
|
+
/** Extra HTTP headers for every request. */
|
|
68
|
+
extraHeaders() {
|
|
69
|
+
return this.config.headers || {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class RetryConfig {
|
|
73
|
+
maxAttempts;
|
|
74
|
+
backoffFactor;
|
|
75
|
+
jitter;
|
|
76
|
+
retryOnStatus;
|
|
77
|
+
constructor(maxAttempts = 3, backoffFactor = 0.5, jitter = true, retryOnStatus = [429, 500, 502, 503, 504]) {
|
|
78
|
+
this.maxAttempts = maxAttempts;
|
|
79
|
+
this.backoffFactor = backoffFactor;
|
|
80
|
+
this.jitter = jitter;
|
|
81
|
+
this.retryOnStatus = retryOnStatus;
|
|
82
|
+
}
|
|
83
|
+
static fromOptions(opts = {}) {
|
|
84
|
+
return new RetryConfig(opts.maxAttempts, opts.backoffFactor, opts.jitter, opts.retryOnStatus);
|
|
85
|
+
}
|
|
86
|
+
shouldRetry(status) {
|
|
87
|
+
return this.retryOnStatus.includes(status);
|
|
88
|
+
}
|
|
89
|
+
sleepForBackoff(attempt) {
|
|
90
|
+
const base = this.backoffFactor * 2 ** attempt;
|
|
91
|
+
return this.jitter ? base * (0.5 + Math.random()) : base;
|
|
92
|
+
}
|
|
93
|
+
async delay(attempt) {
|
|
94
|
+
const ms = this.sleepForBackoff(attempt) * 1000;
|
|
95
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=config.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class LLMError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class ProviderError extends LLMError {
|
|
5
|
+
provider: string;
|
|
6
|
+
statusCode: number | null;
|
|
7
|
+
body?: string | undefined;
|
|
8
|
+
constructor(provider: string, statusCode: number | null, message: string, body?: string | undefined);
|
|
9
|
+
}
|
|
10
|
+
export declare class RateLimitError extends ProviderError {
|
|
11
|
+
constructor(provider: string, statusCode: number | null, message: string, body?: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class StreamInterruptedError extends LLMError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class AllProvidersFailedError extends LLMError {
|
|
17
|
+
attempts: Array<{
|
|
18
|
+
provider: string;
|
|
19
|
+
error: Error;
|
|
20
|
+
}>;
|
|
21
|
+
constructor(attempts: Array<{
|
|
22
|
+
provider: string;
|
|
23
|
+
error: Error;
|
|
24
|
+
}>);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @im-bot/llm-client — error hierarchy
|
|
2
|
+
export class LLMError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'LLMError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ProviderError extends LLMError {
|
|
9
|
+
provider;
|
|
10
|
+
statusCode;
|
|
11
|
+
body;
|
|
12
|
+
constructor(provider, statusCode, message, body) {
|
|
13
|
+
super(`[${provider}] ${statusCode}: ${message}`);
|
|
14
|
+
this.provider = provider;
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
this.body = body;
|
|
17
|
+
this.name = 'ProviderError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class RateLimitError extends ProviderError {
|
|
21
|
+
constructor(provider, statusCode, message, body) {
|
|
22
|
+
super(provider, statusCode, 'rate limited', body);
|
|
23
|
+
this.name = 'RateLimitError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class StreamInterruptedError extends LLMError {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'StreamInterruptedError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class AllProvidersFailedError extends LLMError {
|
|
33
|
+
attempts;
|
|
34
|
+
constructor(attempts) {
|
|
35
|
+
const lines = [`All ${attempts.length} providers failed:`];
|
|
36
|
+
for (const { provider, error } of attempts) {
|
|
37
|
+
lines.push(` - ${provider}: ${String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
super(lines.join('\n'));
|
|
40
|
+
this.attempts = attempts;
|
|
41
|
+
this.name = 'AllProvidersFailedError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { LLMClient } from './client.js';
|
|
2
|
+
export { AsyncLLMClient } from './async-client.js';
|
|
3
|
+
export { Message } from './messages.js';
|
|
4
|
+
export { ToolSpec } from './tools.js';
|
|
5
|
+
export { Provider, RetryConfig, type ProviderConfig, type RetryConfigOptions, type ProviderEnvVar, } from './config.js';
|
|
6
|
+
export { LLMError, ProviderError, RateLimitError, AllProvidersFailedError, StreamInterruptedError, } from './errors.js';
|
|
7
|
+
export type { ChatResponse, StreamChunk, Usage, ToolCall, ChatOptions, StreamOptions, ToolChatOptions, } from './types.js';
|
|
8
|
+
export declare const VERSION = "0.1.0";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @im-bot/llm-client — TypeScript SDK for multi-provider LLM calls.
|
|
2
|
+
export { LLMClient } from './client.js';
|
|
3
|
+
export { AsyncLLMClient } from './async-client.js';
|
|
4
|
+
export { Message } from './messages.js';
|
|
5
|
+
export { ToolSpec } from './tools.js';
|
|
6
|
+
export { Provider, RetryConfig, } from './config.js';
|
|
7
|
+
export { LLMError, ProviderError, RateLimitError, AllProvidersFailedError, StreamInterruptedError, } from './errors.js';
|
|
8
|
+
export const VERSION = '0.1.0';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolCall } from './types.js';
|
|
2
|
+
export declare class Message {
|
|
3
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
4
|
+
content: string;
|
|
5
|
+
name?: string | undefined;
|
|
6
|
+
toolCallId?: string | undefined;
|
|
7
|
+
toolCalls?: ToolCall[] | undefined;
|
|
8
|
+
constructor(role: 'system' | 'user' | 'assistant' | 'tool', content: string, name?: string | undefined, toolCallId?: string | undefined, toolCalls?: ToolCall[] | undefined);
|
|
9
|
+
static system(content: string): Message;
|
|
10
|
+
static user(content: string): Message;
|
|
11
|
+
static assistant(content: string, toolCalls?: ToolCall[]): Message;
|
|
12
|
+
static toolResult(toolCallId: string, name: string, content: string): Message;
|
|
13
|
+
toOpenAIDict(): Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=messages.d.ts.map
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @im-bot/llm-client — Message class
|
|
2
|
+
export class Message {
|
|
3
|
+
role;
|
|
4
|
+
content;
|
|
5
|
+
name;
|
|
6
|
+
toolCallId;
|
|
7
|
+
toolCalls;
|
|
8
|
+
constructor(role, content, name, toolCallId, toolCalls) {
|
|
9
|
+
this.role = role;
|
|
10
|
+
this.content = content;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.toolCallId = toolCallId;
|
|
13
|
+
this.toolCalls = toolCalls;
|
|
14
|
+
}
|
|
15
|
+
static system(content) {
|
|
16
|
+
return new Message('system', content);
|
|
17
|
+
}
|
|
18
|
+
static user(content) {
|
|
19
|
+
return new Message('user', content);
|
|
20
|
+
}
|
|
21
|
+
static assistant(content, toolCalls) {
|
|
22
|
+
return new Message('assistant', content, undefined, undefined, toolCalls);
|
|
23
|
+
}
|
|
24
|
+
static toolResult(toolCallId, name, content) {
|
|
25
|
+
return new Message('tool', content, name, toolCallId);
|
|
26
|
+
}
|
|
27
|
+
toOpenAIDict() {
|
|
28
|
+
const d = { role: this.role, content: this.content };
|
|
29
|
+
if (this.name)
|
|
30
|
+
d.name = this.name;
|
|
31
|
+
if (this.toolCallId)
|
|
32
|
+
d.tool_call_id = this.toolCallId;
|
|
33
|
+
if (this.toolCalls)
|
|
34
|
+
d.tool_calls = this.toolCalls;
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=messages.js.map
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ToolHandlerArgs {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
export type ToolHandler = (args: ToolHandlerArgs) => string | Promise<string> | unknown | Promise<unknown>;
|
|
5
|
+
export interface ToolSpecParams {
|
|
6
|
+
type: 'object';
|
|
7
|
+
properties: Record<string, {
|
|
8
|
+
type: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
enum?: string[];
|
|
11
|
+
}>;
|
|
12
|
+
required?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ToolSpecConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: ToolSpecParams;
|
|
18
|
+
handler: ToolHandler;
|
|
19
|
+
}
|
|
20
|
+
export declare class ToolSpec {
|
|
21
|
+
config: ToolSpecConfig;
|
|
22
|
+
constructor(config: ToolSpecConfig);
|
|
23
|
+
toOpenAIDict(): Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface ToolResult {
|
|
26
|
+
toolCallId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
output: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ToolCallResult {
|
|
31
|
+
tool_call_id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
output: string;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=tools.d.ts.map
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @im-bot/llm-client — ToolSpec
|
|
2
|
+
export class ToolSpec {
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
toOpenAIDict() {
|
|
8
|
+
return {
|
|
9
|
+
type: 'function',
|
|
10
|
+
function: {
|
|
11
|
+
name: this.config.name,
|
|
12
|
+
description: this.config.description,
|
|
13
|
+
parameters: this.config.parameters,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=tools.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface Usage {
|
|
2
|
+
inputTokens: number;
|
|
3
|
+
outputTokens: number;
|
|
4
|
+
totalTokens: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatResponse {
|
|
7
|
+
text: string;
|
|
8
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'error';
|
|
9
|
+
usage: Usage;
|
|
10
|
+
costUsd: number;
|
|
11
|
+
model: string;
|
|
12
|
+
provider: string;
|
|
13
|
+
raw?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface StreamChunk {
|
|
16
|
+
delta: string;
|
|
17
|
+
isFinal: boolean;
|
|
18
|
+
usage: Usage;
|
|
19
|
+
finishReason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolCall {
|
|
22
|
+
id: string;
|
|
23
|
+
type: 'function';
|
|
24
|
+
function: {
|
|
25
|
+
name: string;
|
|
26
|
+
arguments: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface ChatOptions {
|
|
30
|
+
model?: string;
|
|
31
|
+
provider?: string;
|
|
32
|
+
temperature?: number;
|
|
33
|
+
maxTokens?: number;
|
|
34
|
+
tools?: Array<{
|
|
35
|
+
config: {
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export interface StreamOptions {
|
|
41
|
+
model?: string;
|
|
42
|
+
provider?: string;
|
|
43
|
+
temperature?: number;
|
|
44
|
+
maxTokens?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ToolChatOptions extends ChatOptions {
|
|
47
|
+
tools: Array<{
|
|
48
|
+
config: {
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
}>;
|
|
52
|
+
maxToolIterations?: number;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@im-bot/llm-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unified multi-provider LLM client for im-bot and Sun Shuhuan apps \u2014 DeepSeek, OpenRouter, MiniMax, Zhipu, and any OpenAI-compatible provider. TypeScript port of the Python im_bot_llm SDK.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/index.js",
|
|
19
|
+
"dist/index.d.ts",
|
|
20
|
+
"dist/client.js",
|
|
21
|
+
"dist/client.d.ts",
|
|
22
|
+
"dist/config.js",
|
|
23
|
+
"dist/config.d.ts",
|
|
24
|
+
"dist/messages.js",
|
|
25
|
+
"dist/messages.d.ts",
|
|
26
|
+
"dist/tools.js",
|
|
27
|
+
"dist/tools.d.ts",
|
|
28
|
+
"dist/errors.js",
|
|
29
|
+
"dist/errors.d.ts",
|
|
30
|
+
"dist/responses.js",
|
|
31
|
+
"dist/responses.d.ts",
|
|
32
|
+
"dist/async-client.js",
|
|
33
|
+
"dist/async-client.d.ts",
|
|
34
|
+
"dist/types.js",
|
|
35
|
+
"dist/types.d.ts",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"test": "tsc --noEmit -p tsconfig.test.json && node --test --import tsx tests/*.test.ts",
|
|
41
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
42
|
+
"prepare": "npm run build",
|
|
43
|
+
"prepack": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.6.0"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"im-bot",
|
|
56
|
+
"llm",
|
|
57
|
+
"openai-compatible",
|
|
58
|
+
"deepseek",
|
|
59
|
+
"openrouter",
|
|
60
|
+
"minimax",
|
|
61
|
+
"zhipu",
|
|
62
|
+
"agent"
|
|
63
|
+
]
|
|
64
|
+
}
|