plugin-custom-llm 1.2.2 → 1.3.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.
@@ -58,6 +58,90 @@ function getChatOpenAI() {
58
58
  }
59
59
  return _ChatOpenAI;
60
60
  }
61
+ let _ChatOpenAICompletions = null;
62
+ function getChatOpenAICompletions() {
63
+ if (!_ChatOpenAICompletions) {
64
+ const mod = requireFromApp("@langchain/openai");
65
+ _ChatOpenAICompletions = mod.ChatOpenAICompletions;
66
+ }
67
+ return _ChatOpenAICompletions;
68
+ }
69
+ function getToolCallsKey(toolCalls = []) {
70
+ return toolCalls.map((tc) => {
71
+ var _a;
72
+ const id = (tc == null ? void 0 : tc.id) ?? "";
73
+ const name = (tc == null ? void 0 : tc.name) ?? ((_a = tc == null ? void 0 : tc.function) == null ? void 0 : _a.name) ?? "";
74
+ return `${id}:${name}`;
75
+ }).join("|");
76
+ }
77
+ function collectReasoningMap(messages) {
78
+ var _a, _b, _c, _d;
79
+ const reasoningMap = /* @__PURE__ */ new Map();
80
+ for (const message of messages ?? []) {
81
+ if (((_a = message == null ? void 0 : message.getType) == null ? void 0 : _a.call(message)) !== "ai" && ((_b = message == null ? void 0 : message._getType) == null ? void 0 : _b.call(message)) !== "ai") continue;
82
+ if (!((_c = message == null ? void 0 : message.tool_calls) == null ? void 0 : _c.length)) continue;
83
+ const reasoningContent = (_d = message == null ? void 0 : message.additional_kwargs) == null ? void 0 : _d.reasoning_content;
84
+ if (typeof reasoningContent !== "string" || !reasoningContent) continue;
85
+ const key = getToolCallsKey(message.tool_calls);
86
+ if (key) reasoningMap.set(key, reasoningContent);
87
+ }
88
+ return reasoningMap;
89
+ }
90
+ function patchRequestMessagesReasoning(request, reasoningMap) {
91
+ if (!(reasoningMap == null ? void 0 : reasoningMap.size) || !Array.isArray(request == null ? void 0 : request.messages)) return;
92
+ const lastMsg = request.messages.at(-1);
93
+ if ((lastMsg == null ? void 0 : lastMsg.role) !== "tool") return;
94
+ for (const msg of request.messages) {
95
+ if ((msg == null ? void 0 : msg.role) !== "assistant") continue;
96
+ if (!Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) continue;
97
+ if (msg.reasoning_content) continue;
98
+ const key = getToolCallsKey(msg.tool_calls);
99
+ const rc = key ? reasoningMap.get(key) : void 0;
100
+ if (rc) msg.reasoning_content = rc;
101
+ }
102
+ }
103
+ const REASONING_MAP_KEY = "__nb_reasoning_map";
104
+ function createReasoningChatClass() {
105
+ const ChatOpenAICompletions = getChatOpenAICompletions();
106
+ if (!ChatOpenAICompletions) {
107
+ return getChatOpenAI();
108
+ }
109
+ return class ReasoningChatOpenAI extends ChatOpenAICompletions {
110
+ async _generate(messages, options, runManager) {
111
+ const reasoningMap = collectReasoningMap(messages);
112
+ return super._generate(messages, { ...options || {}, [REASONING_MAP_KEY]: reasoningMap }, runManager);
113
+ }
114
+ async *_streamResponseChunks(messages, options, runManager) {
115
+ const reasoningMap = (options == null ? void 0 : options[REASONING_MAP_KEY]) instanceof Map ? options[REASONING_MAP_KEY] : collectReasoningMap(messages);
116
+ yield* super._streamResponseChunks(messages, { ...options || {}, [REASONING_MAP_KEY]: reasoningMap }, runManager);
117
+ }
118
+ _convertCompletionsDeltaToBaseMessageChunk(delta, rawResponse, defaultRole) {
119
+ const messageChunk = super._convertCompletionsDeltaToBaseMessageChunk(delta, rawResponse, defaultRole);
120
+ if (delta == null ? void 0 : delta.reasoning_content) {
121
+ messageChunk.additional_kwargs = {
122
+ ...messageChunk.additional_kwargs || {},
123
+ reasoning_content: delta.reasoning_content
124
+ };
125
+ }
126
+ return messageChunk;
127
+ }
128
+ _convertCompletionsMessageToBaseMessage(message, rawResponse) {
129
+ const langChainMessage = super._convertCompletionsMessageToBaseMessage(message, rawResponse);
130
+ if (message == null ? void 0 : message.reasoning_content) {
131
+ langChainMessage.additional_kwargs = {
132
+ ...langChainMessage.additional_kwargs || {},
133
+ reasoning_content: message.reasoning_content
134
+ };
135
+ }
136
+ return langChainMessage;
137
+ }
138
+ async completionWithRetry(request, requestOptions) {
139
+ const reasoningMap = requestOptions == null ? void 0 : requestOptions[REASONING_MAP_KEY];
140
+ patchRequestMessagesReasoning(request, reasoningMap);
141
+ return super.completionWithRetry(request, requestOptions);
142
+ }
143
+ };
144
+ }
61
145
  let _ChatGenerationChunk = null;
62
146
  function getChatGenerationChunk() {
63
147
  if (!_ChatGenerationChunk) {
@@ -150,12 +234,25 @@ function getByPath(obj, dotPath) {
150
234
  }
151
235
  return current;
152
236
  }
153
- function createMappingFetch(responseMapping) {
237
+ function createMappingFetch(responseMapping, timeoutMs) {
154
238
  const contentPath = responseMapping.content;
155
239
  if (!contentPath) return void 0;
240
+ const toolCallsPath = responseMapping.tool_calls;
241
+ const finishReasonPath = responseMapping.finish_reason;
156
242
  return async (url, init) => {
157
243
  var _a, _b;
158
- const response = await fetch(url, init);
244
+ let response;
245
+ if (timeoutMs && timeoutMs > 0) {
246
+ const controller = new AbortController();
247
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
248
+ try {
249
+ response = await fetch(url, { ...init, signal: controller.signal });
250
+ } finally {
251
+ clearTimeout(timer);
252
+ }
253
+ } else {
254
+ response = await fetch(url, init);
255
+ }
159
256
  if (!response.ok) return response;
160
257
  const contentType = response.headers.get("content-type") || "";
161
258
  if (contentType.includes("text/event-stream") || ((_a = init == null ? void 0 : init.headers) == null ? void 0 : _a["Accept"]) === "text/event-stream") {
@@ -186,17 +283,28 @@ function createMappingFetch(responseMapping) {
186
283
  try {
187
284
  const parsed = JSON.parse(data);
188
285
  const mappedContent = getByPath(parsed, contentPath);
189
- if (mappedContent !== void 0) {
286
+ const mappedToolCalls = toolCallsPath ? getByPath(parsed, toolCallsPath) : getByPath(parsed, "choices.0.delta.tool_calls") ?? getByPath(parsed, "delta.tool_calls");
287
+ const mappedFinishReason = finishReasonPath ? getByPath(parsed, finishReasonPath) : getByPath(parsed, "choices.0.finish_reason") ?? getByPath(parsed, "finish_reason");
288
+ if (mappedContent !== void 0 || mappedToolCalls) {
289
+ const delta = { role: "assistant" };
290
+ if (mappedContent !== void 0) {
291
+ delta.content = String(mappedContent);
292
+ }
293
+ if (mappedToolCalls) {
294
+ delta.tool_calls = mappedToolCalls;
295
+ }
190
296
  const mapped = {
191
297
  id: getByPath(parsed, responseMapping.id || "id") || "chatcmpl-custom",
192
298
  object: "chat.completion.chunk",
193
299
  created: Math.floor(Date.now() / 1e3),
194
300
  model: "custom",
195
- choices: [{
196
- index: 0,
197
- delta: { content: String(mappedContent), role: "assistant" },
198
- finish_reason: null
199
- }]
301
+ choices: [
302
+ {
303
+ index: 0,
304
+ delta,
305
+ finish_reason: mappedFinishReason ?? null
306
+ }
307
+ ]
200
308
  };
201
309
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(mapped)}
202
310
 
@@ -228,21 +336,33 @@ function createMappingFetch(responseMapping) {
228
336
  if (contentType.includes("application/json")) {
229
337
  const body = await response.json();
230
338
  const mappedContent = getByPath(body, contentPath);
231
- if (mappedContent !== void 0) {
339
+ const mappedToolCalls = toolCallsPath ? getByPath(body, toolCallsPath) : getByPath(body, "choices.0.message.tool_calls") ?? getByPath(body, "message.tool_calls");
340
+ const mappedFinishReason = finishReasonPath ? getByPath(body, finishReasonPath) : getByPath(body, "choices.0.finish_reason") ?? getByPath(body, "finish_reason");
341
+ if (mappedContent !== void 0 || mappedToolCalls) {
342
+ const message = {
343
+ role: getByPath(body, responseMapping.role || "") || "assistant"
344
+ };
345
+ if (mappedContent !== void 0) {
346
+ message.content = String(mappedContent);
347
+ } else {
348
+ message.content = null;
349
+ }
350
+ if (mappedToolCalls) {
351
+ message.tool_calls = mappedToolCalls;
352
+ }
232
353
  const mapped = {
233
354
  id: getByPath(body, responseMapping.id || "id") || "chatcmpl-custom",
234
355
  object: "chat.completion",
235
356
  created: Math.floor(Date.now() / 1e3),
236
357
  model: "custom",
237
- choices: [{
238
- index: 0,
239
- message: {
240
- role: getByPath(body, responseMapping.role || "") || "assistant",
241
- content: String(mappedContent)
242
- },
243
- finish_reason: "stop"
244
- }],
245
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
358
+ choices: [
359
+ {
360
+ index: 0,
361
+ message,
362
+ finish_reason: mappedFinishReason ?? (mappedToolCalls ? "tool_calls" : "stop")
363
+ }
364
+ ],
365
+ usage: body.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
246
366
  };
247
367
  return new Response(JSON.stringify(mapped), {
248
368
  status: response.status,
@@ -431,7 +551,15 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
431
551
  }
432
552
  createModel() {
433
553
  var _a;
434
- const { apiKey, disableStream, timeout, streamKeepAlive, keepAliveIntervalMs, keepAliveContent } = this.serviceOptions || {};
554
+ const {
555
+ apiKey,
556
+ disableStream,
557
+ timeout,
558
+ streamKeepAlive,
559
+ keepAliveIntervalMs,
560
+ keepAliveContent,
561
+ enableReasoning
562
+ } = this.serviceOptions || {};
435
563
  const baseURL = (_a = this.serviceOptions) == null ? void 0 : _a.baseURL;
436
564
  const { responseFormat } = this.modelOptions || {};
437
565
  const reqConfig = this.requestConfig;
@@ -446,7 +574,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
446
574
  if (reqConfig.extraBody && typeof reqConfig.extraBody === "object") {
447
575
  Object.assign(modelKwargs, reqConfig.extraBody);
448
576
  }
449
- const ChatOpenAI = getChatOpenAI();
577
+ const ChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
450
578
  const config = {
451
579
  apiKey,
452
580
  ...this.modelOptions,
@@ -459,17 +587,18 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
459
587
  if (disableStream) {
460
588
  config.streaming = false;
461
589
  }
462
- if (timeout && Number(timeout) > 0) {
463
- config.timeout = Number(timeout);
464
- config.configuration.timeout = Number(timeout);
590
+ const timeoutMs = timeout && Number(timeout) > 0 ? Number(timeout) : 0;
591
+ if (timeoutMs) {
592
+ config.timeout = timeoutMs;
593
+ config.configuration.timeout = timeoutMs;
465
594
  }
466
595
  if (reqConfig.extraHeaders && typeof reqConfig.extraHeaders === "object") {
467
596
  config.configuration.defaultHeaders = reqConfig.extraHeaders;
468
597
  }
469
598
  if (resConfig.responseMapping) {
470
- config.configuration.fetch = createMappingFetch(resConfig.responseMapping);
599
+ config.configuration.fetch = createMappingFetch(resConfig.responseMapping, timeoutMs || 12e4);
471
600
  }
472
- let model = new ChatOpenAI(config);
601
+ let model = new ChatClass(config);
473
602
  model = fixEmptyToolProperties(model);
474
603
  if (streamKeepAlive && !disableStream) {
475
604
  return wrapWithStreamKeepAlive(model, {
@@ -505,8 +634,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
505
634
  content.content = textBlocks.map((block) => block.text).join("") || "";
506
635
  }
507
636
  if (typeof content.content === "string") {
508
- const escapedPrefix = KEEPALIVE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
509
- content.content = content.content.replace(new RegExp(escapedPrefix + ".*?(?=" + escapedPrefix + "|$)", "g"), "");
637
+ content.content = content.content.replaceAll(KEEPALIVE_PREFIX, "");
510
638
  content.content = stripToolCallTags(content.content);
511
639
  }
512
640
  if (((_b = (_a = content.metadata) == null ? void 0 : _a.additional_kwargs) == null ? void 0 : _b.__keepalive) !== void 0) {
@@ -640,10 +768,10 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
640
768
  * from DB. Result is cached on `ctx.state._docPixieActive` for the request lifetime.
641
769
  */
642
770
  async hasDocPixieSkill(ctx) {
643
- var _a, _b, _c, _d, _e;
771
+ var _a, _b, _c, _d, _e, _f, _g, _h;
644
772
  if (ctx.state._docPixieActive !== void 0) return ctx.state._docPixieActive;
645
773
  try {
646
- const employeeUsername = (_c = (_b = (_a = ctx.action) == null ? void 0 : _a.params) == null ? void 0 : _b.values) == null ? void 0 : _c.aiEmployee;
774
+ const employeeUsername = ((_c = (_b = (_a = ctx.action) == null ? void 0 : _a.params) == null ? void 0 : _b.values) == null ? void 0 : _c.aiEmployee) ?? ((_e = (_d = ctx.action) == null ? void 0 : _d.params) == null ? void 0 : _e.aiEmployee) ?? ((_f = ctx.state) == null ? void 0 : _f.currentAiEmployee);
647
775
  if (!employeeUsername) {
648
776
  ctx.state._docPixieActive = false;
649
777
  return false;
@@ -652,7 +780,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
652
780
  filter: { username: String(employeeUsername) },
653
781
  fields: ["skillSettings"]
654
782
  });
655
- const skills = ((_e = (_d = employee == null ? void 0 : employee.get) == null ? void 0 : _d.call(employee, "skillSettings")) == null ? void 0 : _e.skills) ?? [];
783
+ const skills = ((_h = (_g = employee == null ? void 0 : employee.get) == null ? void 0 : _g.call(employee, "skillSettings")) == null ? void 0 : _h.skills) ?? [];
656
784
  const has = skills.some((s) => s.name === "docpixie.query.document");
657
785
  ctx.state._docPixieActive = has;
658
786
  return has;
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "plugin-custom-llm",
3
- "displayName": "AI LLM: Custom (OpenAI Compatible)",
4
- "displayName.zh-CN": "AI LLM:自定义(OpenAI 兼容)",
5
- "description": "OpenAI-compatible LLM provider with auto response format detection for external LLM services.",
6
- "version": "1.2.2",
7
- "main": "dist/server/index.js",
8
- "files": [
9
- "dist",
10
- "client.js",
11
- "client.d.ts",
12
- "server.js",
13
- "server.d.ts",
14
- "src"
15
- ],
16
- "nocobase": {
17
- "supportedVersions": [
18
- "2.x"
19
- ],
20
- "editionLevel": 0
21
- },
22
- "dependencies": {
23
- "@langchain/openai": "^1.0.0",
24
- "@langchain/core": "^1.0.0"
25
- },
26
- "peerDependencies": {
27
- "@nocobase/client": "2.x",
28
- "@nocobase/plugin-ai": "2.x",
29
- "@nocobase/server": "2.x",
30
- "@nocobase/test": "2.x"
31
- },
32
- "keywords": [
33
- "AI"
34
- ],
35
- "license": "Apache-2.0"
36
- }
1
+ {
2
+ "name": "plugin-custom-llm",
3
+ "displayName": "AI LLM: Custom (OpenAI Compatible)",
4
+ "displayName.zh-CN": "AI LLM:自定义(OpenAI 兼容)",
5
+ "description": "OpenAI-compatible LLM provider with auto response format detection for external LLM services.",
6
+ "version": "1.3.0",
7
+ "main": "dist/server/index.js",
8
+ "files": [
9
+ "dist",
10
+ "client.js",
11
+ "client.d.ts",
12
+ "server.js",
13
+ "server.d.ts",
14
+ "src"
15
+ ],
16
+ "nocobase": {
17
+ "supportedVersions": [
18
+ "2.x"
19
+ ],
20
+ "editionLevel": 0
21
+ },
22
+ "dependencies": {
23
+ "@langchain/openai": "^1.0.0",
24
+ "@langchain/core": "^1.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@nocobase/client": "2.x",
28
+ "@nocobase/plugin-ai": "2.x",
29
+ "@nocobase/server": "2.x",
30
+ "@nocobase/test": "2.x"
31
+ },
32
+ "keywords": [
33
+ "AI"
34
+ ],
35
+ "license": "Apache-2.0"
36
+ }
@@ -7,6 +7,15 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ /**
11
+ * This file is part of the NocoBase (R) project.
12
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
13
+ * Authors: NocoBase Team.
14
+ *
15
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
16
+ * For more information, please refer to: https://www.nocobase.com/agreement.
17
+ */
18
+
10
19
  // CSS modules
11
20
  type CSSModuleClasses = { readonly [key: string]: string };
12
21
 
@@ -1,19 +1,28 @@
1
- import { Plugin } from '@nocobase/client';
2
- import PluginAIClient from '@nocobase/plugin-ai/client';
3
- import { customLLMProviderOptions } from './llm-providers/custom-llm';
4
-
5
- export class PluginCustomLLMClient extends Plugin {
6
- async afterAdd() {}
7
-
8
- async beforeLoad() {}
9
-
10
- async load() {
11
- this.aiPlugin.aiManager.registerLLMProvider('custom-llm', customLLMProviderOptions);
12
- }
13
-
14
- private get aiPlugin(): PluginAIClient {
15
- return this.app.pm.get('ai');
16
- }
17
- }
18
-
19
- export default PluginCustomLLMClient;
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { Plugin } from '@nocobase/client';
11
+ import PluginAIClient from '@nocobase/plugin-ai/client';
12
+ import { customLLMProviderOptions } from './llm-providers/custom-llm';
13
+
14
+ export class PluginCustomLLMClient extends Plugin {
15
+ async afterAdd() {}
16
+
17
+ async beforeLoad() {}
18
+
19
+ async load() {
20
+ this.aiPlugin.aiManager.registerLLMProvider('custom-llm', customLLMProviderOptions);
21
+ }
22
+
23
+ private get aiPlugin(): PluginAIClient {
24
+ return this.app.pm.get('ai');
25
+ }
26
+ }
27
+
28
+ export default PluginCustomLLMClient;