@nickyzj2023/ai 1.4.4 → 1.6.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 +80 -64
- package/dist/cli.mjs +163 -21
- package/dist/helper-juLVGxlQ.mjs +65 -0
- package/dist/index.d.mts +29 -197
- package/dist/index.mjs +3 -2
- package/dist/mcp-DcntQg-a.mjs +58 -0
- package/dist/{src-CuZ-6Jye.mjs → src-CKKPKPMG.mjs} +114 -192
- package/dist/tools/mcp.d.mts +55 -0
- package/dist/tools/mcp.mjs +2 -0
- package/dist/types--dXFoUow.d.mts +144 -0
- package/package.json +6 -2
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1
|
+
import { i as estimateTokens, n as defineTool, r as estimateTextTokens } from "./helper-juLVGxlQ.mjs";
|
|
2
|
+
import { createXMLText, extractErrorMessage, fetcher, logger, parseSSE, pick, to } from "@nickyzj2023/utils";
|
|
4
3
|
//#region src/llm.ts
|
|
5
4
|
/**
|
|
6
5
|
* 分离ToolDefinition中的valid/invalid字段,前者可以传给模型,后者用于本地运算
|
|
@@ -22,7 +21,7 @@ const extractReasoning = (msgLike) => {
|
|
|
22
21
|
/**
|
|
23
22
|
* 流式请求模型,转发
|
|
24
23
|
*/
|
|
25
|
-
async function* stream(model, messages, tools = []) {
|
|
24
|
+
async function* stream(model, messages, tools = [], options) {
|
|
26
25
|
const validTools = tools.map((tool) => detachToolArguments(tool)[0]);
|
|
27
26
|
const api = fetcher(model.baseUrl, {
|
|
28
27
|
headers: { Authorization: `Bearer ${model.apiKey}` },
|
|
@@ -32,9 +31,11 @@ async function* stream(model, messages, tools = []) {
|
|
|
32
31
|
stream: true,
|
|
33
32
|
model: model.model,
|
|
34
33
|
messages,
|
|
35
|
-
tools: validTools
|
|
36
|
-
|
|
34
|
+
tools: validTools,
|
|
35
|
+
...options?.body
|
|
36
|
+
}, options));
|
|
37
37
|
if (error) {
|
|
38
|
+
if (options?.signal?.aborted) return;
|
|
38
39
|
yield {
|
|
39
40
|
type: "error",
|
|
40
41
|
message: error.message
|
|
@@ -44,39 +45,48 @@ async function* stream(model, messages, tools = []) {
|
|
|
44
45
|
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
45
46
|
let usage;
|
|
46
47
|
let finishReason = null;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
48
|
+
try {
|
|
49
|
+
for await (const chunk of parseSSE(response)) {
|
|
50
|
+
if (typeof chunk === "string") continue;
|
|
51
|
+
if (chunk.usage) usage = chunk.usage;
|
|
52
|
+
const choice = chunk.choices?.[0];
|
|
53
|
+
if (!choice) continue;
|
|
54
|
+
const { delta } = choice;
|
|
55
|
+
const { content: contentDelta, tool_calls: toolCalls } = delta;
|
|
56
|
+
const reasoning = extractReasoning(delta);
|
|
57
|
+
if (reasoning) yield {
|
|
58
|
+
type: "reasoning_delta",
|
|
59
|
+
delta: reasoning
|
|
60
|
+
};
|
|
61
|
+
if (contentDelta) yield {
|
|
62
|
+
type: "content_delta",
|
|
63
|
+
delta: contentDelta.toString()
|
|
64
|
+
};
|
|
65
|
+
if (toolCalls) for (const call of toolCalls) {
|
|
66
|
+
const { index = 0, type = "function", id, function: fn, ...extra } = call;
|
|
67
|
+
const existing = toolCallBuffers.getOrInsert(index, {
|
|
68
|
+
id: "",
|
|
69
|
+
type,
|
|
70
|
+
function: {
|
|
71
|
+
name: "",
|
|
72
|
+
arguments: ""
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (id) existing.id = id;
|
|
76
|
+
if (fn?.name) existing.function.name += fn.name;
|
|
77
|
+
if (fn?.arguments) existing.function.arguments += fn.arguments;
|
|
78
|
+
if (extra) Object.assign(existing, extra);
|
|
79
|
+
toolCallBuffers.set(index, existing);
|
|
80
|
+
}
|
|
81
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
78
82
|
}
|
|
79
|
-
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (options?.signal?.aborted) return;
|
|
85
|
+
yield {
|
|
86
|
+
type: "error",
|
|
87
|
+
message: extractErrorMessage(e)
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
80
90
|
}
|
|
81
91
|
for (const [, call] of toolCallBuffers) yield {
|
|
82
92
|
type: "tool_call",
|
|
@@ -96,27 +106,48 @@ async function* runAgent(model, messages, tools) {
|
|
|
96
106
|
const toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));
|
|
97
107
|
while (true) {
|
|
98
108
|
let content = "";
|
|
109
|
+
let reasoning = "";
|
|
99
110
|
const toolCalls = [];
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
const promptMessage = messages.slice(-1);
|
|
112
|
+
let startTime = 0;
|
|
113
|
+
for await (const e of stream(model, messages, tools)) {
|
|
114
|
+
startTime ||= Date.now();
|
|
115
|
+
switch (e.type) {
|
|
116
|
+
case "reasoning_delta":
|
|
117
|
+
reasoning += e.delta;
|
|
118
|
+
yield e;
|
|
119
|
+
break;
|
|
120
|
+
case "content_delta":
|
|
121
|
+
content += e.delta;
|
|
122
|
+
yield e;
|
|
123
|
+
break;
|
|
124
|
+
case "tool_call":
|
|
125
|
+
toolCalls.push({
|
|
126
|
+
id: e.id,
|
|
127
|
+
type: "function",
|
|
128
|
+
function: {
|
|
129
|
+
name: e.name,
|
|
130
|
+
arguments: e.args
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
yield e;
|
|
134
|
+
break;
|
|
135
|
+
case "error":
|
|
136
|
+
yield e;
|
|
137
|
+
break;
|
|
138
|
+
case "done":
|
|
139
|
+
if (!e.usage) {
|
|
140
|
+
const promptTokens = estimateTokens(promptMessage);
|
|
141
|
+
const completionTokens = estimateTextTokens(content + reasoning);
|
|
142
|
+
e.usage = {
|
|
143
|
+
prompt_tokens: promptTokens,
|
|
144
|
+
completion_tokens: completionTokens,
|
|
145
|
+
total_tokens: estimateTokens(messages) + promptTokens + completionTokens
|
|
146
|
+
};
|
|
115
147
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
case "done": yield e;
|
|
148
|
+
e.usage.speed = Math.floor(e.usage.completion_tokens / ((Date.now() - startTime) / 1e3));
|
|
149
|
+
yield e;
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
const message = {
|
|
122
153
|
role: "assistant",
|
|
@@ -149,70 +180,6 @@ async function* runAgent(model, messages, tools) {
|
|
|
149
180
|
}
|
|
150
181
|
}
|
|
151
182
|
//#endregion
|
|
152
|
-
//#region src/utils/helper.ts
|
|
153
|
-
/**
|
|
154
|
-
* 辅助定义一个POST /chat/completions支持的model参数
|
|
155
|
-
* @remarks 只有baseUrl字段是必须的
|
|
156
|
-
*/
|
|
157
|
-
const defineModel = (config) => ({
|
|
158
|
-
modalities: ["text"],
|
|
159
|
-
context: 131072,
|
|
160
|
-
...config
|
|
161
|
-
});
|
|
162
|
-
/**
|
|
163
|
-
* 辅助定义一个POST /chat/completions支持的tool对象
|
|
164
|
-
* @param execute 实际执行工具的函数
|
|
165
|
-
*/
|
|
166
|
-
const defineTool = (name, description, properties, execute) => {
|
|
167
|
-
const _required = [];
|
|
168
|
-
return {
|
|
169
|
-
type: "function",
|
|
170
|
-
function: {
|
|
171
|
-
name,
|
|
172
|
-
description,
|
|
173
|
-
parameters: {
|
|
174
|
-
type: "object",
|
|
175
|
-
properties: Object.entries(properties).reduce((result, [key, property]) => {
|
|
176
|
-
if ("required" in property) {
|
|
177
|
-
_required.push(key);
|
|
178
|
-
delete property.required;
|
|
179
|
-
}
|
|
180
|
-
result[key] = property;
|
|
181
|
-
return result;
|
|
182
|
-
}, {}),
|
|
183
|
-
required: _required
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
execute
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
/**
|
|
190
|
-
* 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
|
|
191
|
-
* - 单词 ≈ 1.5token
|
|
192
|
-
* - 标点/空白等非词字符每 4 个 ≈ 1token
|
|
193
|
-
* - 图片/音频/视频/文件 ≈ 4096token(不好估算,取个较大的值)
|
|
194
|
-
*/
|
|
195
|
-
const estimateTokens = (messages) => {
|
|
196
|
-
if (!messages?.length) return 0;
|
|
197
|
-
const segmenter = new Intl.Segmenter([], { granularity: "word" });
|
|
198
|
-
const estimateTextTokens = (text) => {
|
|
199
|
-
let words = 0;
|
|
200
|
-
let others = 0;
|
|
201
|
-
for (const seg of segmenter.segment(text)) if (seg.isWordLike) words++;
|
|
202
|
-
else others++;
|
|
203
|
-
return Math.ceil(words * 1.5 + others / 4);
|
|
204
|
-
};
|
|
205
|
-
return messages.reduce((acc, message) => {
|
|
206
|
-
const { content, tool_calls, ...metadata } = message;
|
|
207
|
-
if (typeof content === "string") acc += estimateTextTokens(content);
|
|
208
|
-
else for (const part of content) if (part.type === "text") acc += estimateTextTokens(part.text);
|
|
209
|
-
else acc += 4096;
|
|
210
|
-
if (tool_calls) acc += estimateTextTokens(JSON.stringify(tool_calls));
|
|
211
|
-
acc += estimateTextTokens(JSON.stringify(metadata));
|
|
212
|
-
return acc;
|
|
213
|
-
}, 0);
|
|
214
|
-
};
|
|
215
|
-
//#endregion
|
|
216
183
|
//#region src/tools/get-time.ts
|
|
217
184
|
var get_time_default = defineTool("get_time", "查询指定时区的当前时间", { timezone: {
|
|
218
185
|
type: "string",
|
|
@@ -231,60 +198,6 @@ var get_weather_default = defineTool("get_weather", "查询指定城市的天气
|
|
|
231
198
|
return fetcher("https://wttr.in", { params: { format: "j1" } }).get(`/${city}`);
|
|
232
199
|
});
|
|
233
200
|
//#endregion
|
|
234
|
-
//#region src/tools/mcp.ts
|
|
235
|
-
/** 全局单例MCP加载器 */
|
|
236
|
-
let router = null;
|
|
237
|
-
var MCPRouter = class {
|
|
238
|
-
entries = /* @__PURE__ */ new Map();
|
|
239
|
-
/** 注册一个新的MCP客户端 */
|
|
240
|
-
async addClient(name, url, options) {
|
|
241
|
-
if (this.entries.has(name)) return;
|
|
242
|
-
const transport = new StreamableHTTPClientTransport(new URL(url), { requestInit: { headers: options?.headers } });
|
|
243
|
-
const client = new Client({
|
|
244
|
-
name,
|
|
245
|
-
version: "1.0.0"
|
|
246
|
-
});
|
|
247
|
-
await client.connect(transport);
|
|
248
|
-
const { tools } = await client.listTools();
|
|
249
|
-
const normalizedTools = tools.filter((tool) => !options?.ignoredToolNames?.includes(tool.name)).map((tool) => {
|
|
250
|
-
const _properties = { ...tool.inputSchema.properties ?? {} };
|
|
251
|
-
tool.inputSchema.required?.forEach((key) => {
|
|
252
|
-
if (isObject(_properties[key])) _properties[key] = {
|
|
253
|
-
..._properties[key],
|
|
254
|
-
required: true
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
return defineTool(tool.name, tool.description ?? "", _properties, (args) => client.callTool({
|
|
258
|
-
name: tool.name,
|
|
259
|
-
arguments: args
|
|
260
|
-
}));
|
|
261
|
-
});
|
|
262
|
-
this.entries.set(name, {
|
|
263
|
-
client,
|
|
264
|
-
tools: normalizedTools
|
|
265
|
-
});
|
|
266
|
-
return client;
|
|
267
|
-
}
|
|
268
|
-
/** 返回OpenAI API兼容的tools数组 */
|
|
269
|
-
async getTools() {
|
|
270
|
-
return [...this.entries.values()].flatMap((e) => e.tools);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
/**
|
|
274
|
-
* 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
|
|
275
|
-
*/
|
|
276
|
-
const loadMCPTools = async (mcpServers = {}) => {
|
|
277
|
-
router ||= new MCPRouter();
|
|
278
|
-
await Promise.allSettled(Object.entries(mcpServers).map(async ([name, server]) => {
|
|
279
|
-
try {
|
|
280
|
-
await router?.addClient(name, server.url, omit(server, ["type", "url"]));
|
|
281
|
-
} catch (e) {
|
|
282
|
-
logger(`MCP服务器${name}加载失败:${extractErrorMessage(e)}`);
|
|
283
|
-
}
|
|
284
|
-
}));
|
|
285
|
-
return router.getTools();
|
|
286
|
-
};
|
|
287
|
-
//#endregion
|
|
288
201
|
//#region src/utils/compact/helper.ts
|
|
289
202
|
/**
|
|
290
203
|
* 校验assistant(tool_calls)消息
|
|
@@ -296,12 +209,11 @@ const isToolCalls = (message) => {
|
|
|
296
209
|
* 校验多模态消息
|
|
297
210
|
*/
|
|
298
211
|
const isMediaMessage = (message) => {
|
|
299
|
-
const
|
|
300
|
-
"image_url"
|
|
301
|
-
"input_audio"
|
|
302
|
-
"video_url"
|
|
303
|
-
|
|
304
|
-
return message && Array.isArray(message.content) && message.content.some((part) => MEDIA_TYPES.includes(part.type));
|
|
212
|
+
if (message && Array.isArray(message.content)) for (const part of message.content) {
|
|
213
|
+
if (part.type === "image_url") return "image";
|
|
214
|
+
if (part.type === "input_audio") return "audio";
|
|
215
|
+
if (part.type === "video_url") return "video";
|
|
216
|
+
}
|
|
305
217
|
};
|
|
306
218
|
/**
|
|
307
219
|
* 查找assistant(tool_calls) + tool配对组范围
|
|
@@ -330,7 +242,8 @@ const defaultReplacerOfToolResultContent = async (content, options) => {
|
|
|
330
242
|
content: "请用一句话简述上面这条消息(直击要点,不要添加任何前言后语)"
|
|
331
243
|
}];
|
|
332
244
|
let simplifiedContent = "";
|
|
333
|
-
|
|
245
|
+
if (!model) simplifiedContent = "工具结果已被消费";
|
|
246
|
+
else for await (const e of runAgent(model, messages, [])) if (e.type === "content_delta") simplifiedContent += e.delta;
|
|
334
247
|
else if (e.type === "error") throw new Error(e.message);
|
|
335
248
|
return simplifiedContent;
|
|
336
249
|
};
|
|
@@ -352,7 +265,7 @@ const compactToolResults = async (compressible, options) => {
|
|
|
352
265
|
};
|
|
353
266
|
/** 默认的压缩多模态消息策略:让大模型精简消息内容 */
|
|
354
267
|
const defaultReplacerOfMediaContent = async (content, options) => {
|
|
355
|
-
const { model } = options ?? {};
|
|
268
|
+
const { model, modality = "image" } = options ?? {};
|
|
356
269
|
const messages = [{
|
|
357
270
|
role: "user",
|
|
358
271
|
content
|
|
@@ -361,7 +274,8 @@ const defaultReplacerOfMediaContent = async (content, options) => {
|
|
|
361
274
|
content: "请用一句话简述上面这条消息(直击要点,不要添加任何前言后语)"
|
|
362
275
|
}];
|
|
363
276
|
let simplifiedContent = "";
|
|
364
|
-
|
|
277
|
+
if (!model?.modalities?.includes(modality)) simplifiedContent = "媒体资源已被消费";
|
|
278
|
+
else for await (const e of runAgent(model, messages, [])) if (e.type === "content_delta") simplifiedContent += e.delta;
|
|
365
279
|
else if (e.type === "error") throw new Error(e.message);
|
|
366
280
|
return simplifiedContent;
|
|
367
281
|
};
|
|
@@ -373,21 +287,28 @@ const compactMediaMessages = async (compressible, options) => {
|
|
|
373
287
|
if (!replacer && !model) return 0;
|
|
374
288
|
const _replacer = replacer || defaultReplacerOfMediaContent;
|
|
375
289
|
let count = 0;
|
|
376
|
-
for (const message of compressible)
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
290
|
+
for (const message of compressible) {
|
|
291
|
+
const mediaModality = isMediaMessage(message);
|
|
292
|
+
if (mediaModality) {
|
|
293
|
+
const compacted = await _replacer(message.content, {
|
|
294
|
+
model,
|
|
295
|
+
modality: mediaModality
|
|
296
|
+
});
|
|
297
|
+
message.content = createXMLText("media", compacted);
|
|
298
|
+
count++;
|
|
299
|
+
}
|
|
380
300
|
}
|
|
381
301
|
if (count > 0) logger(`压缩了${count}条多模态消息`);
|
|
382
302
|
return count;
|
|
383
303
|
};
|
|
384
304
|
const summarizeMessages = async (compressible, options) => {
|
|
385
|
-
const { model, systemPrompt } = options ?? {};
|
|
305
|
+
const { model, systemPrompt, beforeSummarize } = options ?? {};
|
|
386
306
|
const summarizable = compressible.slice(1);
|
|
387
307
|
if (summarizable.length === 0) {
|
|
388
308
|
logger("消息太少,无需总结");
|
|
389
309
|
return 0;
|
|
390
310
|
}
|
|
311
|
+
beforeSummarize?.(summarizable);
|
|
391
312
|
const count = summarizable.length;
|
|
392
313
|
summarizable.push({
|
|
393
314
|
role: "system",
|
|
@@ -459,10 +380,11 @@ const compact = Object.assign(async (messages, model, options) => {
|
|
|
459
380
|
model
|
|
460
381
|
}) > 0;
|
|
461
382
|
if (tokens > context * ratioToSummarize) {
|
|
462
|
-
const { systemPrompt = "你现在的任务是总结历史消息" } = summarizeOptions ?? {};
|
|
383
|
+
const { systemPrompt = "你现在的任务是总结历史消息", beforeSummarize } = summarizeOptions ?? {};
|
|
463
384
|
const [error, count] = await to(summarizeMessages(compressible, {
|
|
464
385
|
model,
|
|
465
|
-
systemPrompt
|
|
386
|
+
systemPrompt,
|
|
387
|
+
beforeSummarize
|
|
466
388
|
}));
|
|
467
389
|
if (!error) result.hasSummarized = count > 0;
|
|
468
390
|
else result.hasDiscardMessages = discardMessagesUntil(compressible, (compressible) => {
|
|
@@ -484,4 +406,4 @@ const compact = Object.assign(async (messages, model, options) => {
|
|
|
484
406
|
discardMessagesUntil
|
|
485
407
|
});
|
|
486
408
|
//#endregion
|
|
487
|
-
export {
|
|
409
|
+
export { stream as a, runAgent as i, get_weather_default as n, get_time_default as r, compact as t };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { p as ToolDefinition } from "../types--dXFoUow.mjs";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
3
|
+
//#region src/tools/mcp.d.ts
|
|
4
|
+
type McpServer = {
|
|
5
|
+
type: "streamable_http" | "sse";
|
|
6
|
+
url: string;
|
|
7
|
+
headers?: Record<string, any>;
|
|
8
|
+
ignoredToolNames?: string[];
|
|
9
|
+
};
|
|
10
|
+
declare class MCPRouter {
|
|
11
|
+
private entries;
|
|
12
|
+
/** 注册一个新的MCP客户端 */
|
|
13
|
+
addClient(name: string, url: string, options?: Omit<McpServer, "type" | "url">): Promise<Client<{
|
|
14
|
+
method: string;
|
|
15
|
+
params?: {
|
|
16
|
+
[x: string]: unknown;
|
|
17
|
+
_meta?: {
|
|
18
|
+
[x: string]: unknown;
|
|
19
|
+
progressToken?: string | number | undefined;
|
|
20
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
21
|
+
taskId: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
method: string;
|
|
27
|
+
params?: {
|
|
28
|
+
[x: string]: unknown;
|
|
29
|
+
_meta?: {
|
|
30
|
+
[x: string]: unknown;
|
|
31
|
+
progressToken?: string | number | undefined;
|
|
32
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
33
|
+
taskId: string;
|
|
34
|
+
} | undefined;
|
|
35
|
+
} | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
[x: string]: unknown;
|
|
39
|
+
_meta?: {
|
|
40
|
+
[x: string]: unknown;
|
|
41
|
+
progressToken?: string | number | undefined;
|
|
42
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
43
|
+
taskId: string;
|
|
44
|
+
} | undefined;
|
|
45
|
+
} | undefined;
|
|
46
|
+
}> | undefined>;
|
|
47
|
+
/** 返回OpenAI API兼容的tools数组 */
|
|
48
|
+
getTools(): Promise<ToolDefinition[]>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 把传入的MCPServer列表转换成OpenAI API兼容的tools数组
|
|
52
|
+
*/
|
|
53
|
+
declare const loadMCPTools: (mcpServers?: Record<string, McpServer>) => Promise<ToolDefinition[]>;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { MCPRouter, McpServer, loadMCPTools };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type Model = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** 如果不传,会尝试使用GET {baseUrl}/models获取到的第一个模型 */
|
|
5
|
+
model?: string;
|
|
6
|
+
/** 使用本地llama.cpp等服务时可以不填 */
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/**
|
|
9
|
+
* 模型支持的消息输入类型
|
|
10
|
+
* @default ["text"]
|
|
11
|
+
* @remarks 会在每轮对话前校验消息类型,存在不支持的输入时报错
|
|
12
|
+
*/
|
|
13
|
+
modalities?: Modality[];
|
|
14
|
+
/**
|
|
15
|
+
* 模型的最大上下文
|
|
16
|
+
* @default 131072
|
|
17
|
+
* @remarks 会在每轮对话结束后检查上下文阈值,自动压缩消息
|
|
18
|
+
*/
|
|
19
|
+
context?: number;
|
|
20
|
+
/**
|
|
21
|
+
* 自定义请求体,会在每轮请求时带上
|
|
22
|
+
* @example chat_template_kwargs: { enable_thinking: false }
|
|
23
|
+
*/
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
/** 文/图/音/视频,暂未实现文件输入 */
|
|
27
|
+
type Modality = "text" | "image" | "audio" | "video";
|
|
28
|
+
type Message = {
|
|
29
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
30
|
+
reasoning?: string | null;
|
|
31
|
+
content: string | ContentPart[];
|
|
32
|
+
tool_calls?: ToolCall[];
|
|
33
|
+
tool_call_id?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
type ContentPart = TextContent | ImageContent | AudioContent | VideoContent;
|
|
37
|
+
type TextContent = {
|
|
38
|
+
type: "text";
|
|
39
|
+
text: string;
|
|
40
|
+
};
|
|
41
|
+
type ImageContent = {
|
|
42
|
+
type: "image_url";
|
|
43
|
+
image_url: {
|
|
44
|
+
url: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
type AudioContent = {
|
|
48
|
+
type: "input_audio";
|
|
49
|
+
input_audio: {
|
|
50
|
+
/** 使用公网可访问的音频链接 */
|
|
51
|
+
url?: string;
|
|
52
|
+
/** 使用base64 */
|
|
53
|
+
data?: string;
|
|
54
|
+
format: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
type VideoContent = {
|
|
58
|
+
type: "video_url";
|
|
59
|
+
video_url: {
|
|
60
|
+
url: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
type ToolDefinition = {
|
|
64
|
+
type: "function";
|
|
65
|
+
function: {
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
parameters: {
|
|
69
|
+
type: "object";
|
|
70
|
+
properties: Record<string, {
|
|
71
|
+
type: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
/** defineTool()提供的语法糖,等价于在parameters.required.push(当前property key) */
|
|
74
|
+
required?: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
required?: string[];
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/** 工具的实际执行函数 */
|
|
80
|
+
execute: (...args: any) => any;
|
|
81
|
+
};
|
|
82
|
+
type ToolCall = {
|
|
83
|
+
id: string;
|
|
84
|
+
type: "function";
|
|
85
|
+
function: {
|
|
86
|
+
name: string;
|
|
87
|
+
arguments: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/** stream()对外输出的事件 */
|
|
91
|
+
type LLMEvent = {
|
|
92
|
+
type: "reasoning_delta";
|
|
93
|
+
delta: string;
|
|
94
|
+
} | {
|
|
95
|
+
type: "content_delta";
|
|
96
|
+
delta: string;
|
|
97
|
+
} | {
|
|
98
|
+
type: "tool_call";
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
args: any;
|
|
102
|
+
} | {
|
|
103
|
+
type: "done";
|
|
104
|
+
finishReason: FinishReason;
|
|
105
|
+
usage?: Usage;
|
|
106
|
+
} | {
|
|
107
|
+
type: "error";
|
|
108
|
+
message: string;
|
|
109
|
+
};
|
|
110
|
+
/** stream()内部要处理的事件流 */
|
|
111
|
+
type ChatCompletionsChunk = {
|
|
112
|
+
id: string;
|
|
113
|
+
object: "chat.completion.chunk";
|
|
114
|
+
created: number;
|
|
115
|
+
model: string;
|
|
116
|
+
choices: {
|
|
117
|
+
index: number;
|
|
118
|
+
delta: Pick<Message, "reasoning" | "content"> & {
|
|
119
|
+
tool_calls?: ({
|
|
120
|
+
index: number;
|
|
121
|
+
} & Partial<ToolCall>)[];
|
|
122
|
+
};
|
|
123
|
+
finish_reason: FinishReason;
|
|
124
|
+
}[];
|
|
125
|
+
usage?: Usage;
|
|
126
|
+
} | "[DONE]";
|
|
127
|
+
/** 模型可能返回的finish_reason值:停止/工具调用/上下文超限 */
|
|
128
|
+
type FinishReason = "stop" | "tool_calls" | "length" | null;
|
|
129
|
+
type Usage = {
|
|
130
|
+
prompt_tokens: number;
|
|
131
|
+
completion_tokens: number;
|
|
132
|
+
total_tokens: number;
|
|
133
|
+
/** 每秒生成多少个token */
|
|
134
|
+
speed?: number;
|
|
135
|
+
[key: string]: any;
|
|
136
|
+
};
|
|
137
|
+
type AgentEvent = LLMEvent | {
|
|
138
|
+
type: "tool_result";
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
result: string;
|
|
142
|
+
};
|
|
143
|
+
//#endregion
|
|
144
|
+
export { FinishReason as a, Message as c, TextContent as d, ToolCall as f, VideoContent as h, ContentPart as i, Modality as l, Usage as m, AudioContent as n, ImageContent as o, ToolDefinition as p, ChatCompletionsChunk as r, LLMEvent as s, AgentEvent as t, Model as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nickyzj2023/ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "我的“pi”,参考了pi-from-scratch",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.mts",
|
|
11
11
|
"import": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./mcp": {
|
|
14
|
+
"types": "./dist/tools/mcp.d.mts",
|
|
15
|
+
"import": "./dist/tools/mcp.mjs"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"@biomejs/biome": "^2.5.12",
|
|
30
|
-
"@types/node": "^26.
|
|
34
|
+
"@types/node": "^26.5.0",
|
|
31
35
|
"tsdown": "^0.22.14",
|
|
32
36
|
"typescript": "^7.0.2"
|
|
33
37
|
},
|