plugin-custom-llm 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,6 +58,95 @@ 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 sanitizeToolCallId(id) {
70
+ if (!id || typeof id !== "string") return id;
71
+ const idx = id.indexOf("__thought__");
72
+ return idx !== -1 ? id.substring(0, idx) : id;
73
+ }
74
+ function getToolCallsKey(toolCalls = []) {
75
+ return toolCalls.map((tc) => {
76
+ var _a;
77
+ const id = (tc == null ? void 0 : tc.id) ?? "";
78
+ const name = (tc == null ? void 0 : tc.name) ?? ((_a = tc == null ? void 0 : tc.function) == null ? void 0 : _a.name) ?? "";
79
+ return `${id}:${name}`;
80
+ }).join("|");
81
+ }
82
+ function collectReasoningMap(messages) {
83
+ var _a, _b, _c, _d;
84
+ const reasoningMap = /* @__PURE__ */ new Map();
85
+ for (const message of messages ?? []) {
86
+ 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;
87
+ if (!((_c = message == null ? void 0 : message.tool_calls) == null ? void 0 : _c.length)) continue;
88
+ const reasoningContent = (_d = message == null ? void 0 : message.additional_kwargs) == null ? void 0 : _d.reasoning_content;
89
+ if (typeof reasoningContent !== "string" || !reasoningContent) continue;
90
+ const key = getToolCallsKey(message.tool_calls);
91
+ if (key) reasoningMap.set(key, reasoningContent);
92
+ }
93
+ return reasoningMap;
94
+ }
95
+ function patchRequestMessagesReasoning(request, reasoningMap) {
96
+ if (!(reasoningMap == null ? void 0 : reasoningMap.size) || !Array.isArray(request == null ? void 0 : request.messages)) return;
97
+ const lastMsg = request.messages.at(-1);
98
+ if ((lastMsg == null ? void 0 : lastMsg.role) !== "tool") return;
99
+ for (const msg of request.messages) {
100
+ if ((msg == null ? void 0 : msg.role) !== "assistant") continue;
101
+ if (!Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) continue;
102
+ if (msg.reasoning_content) continue;
103
+ const key = getToolCallsKey(msg.tool_calls);
104
+ const rc = key ? reasoningMap.get(key) : void 0;
105
+ if (rc) msg.reasoning_content = rc;
106
+ }
107
+ }
108
+ const REASONING_MAP_KEY = "__nb_reasoning_map";
109
+ function createReasoningChatClass() {
110
+ const ChatOpenAICompletions = getChatOpenAICompletions();
111
+ if (!ChatOpenAICompletions) {
112
+ return getChatOpenAI();
113
+ }
114
+ return class ReasoningChatOpenAI extends ChatOpenAICompletions {
115
+ async _generate(messages, options, runManager) {
116
+ const reasoningMap = collectReasoningMap(messages);
117
+ return super._generate(messages, { ...options || {}, [REASONING_MAP_KEY]: reasoningMap }, runManager);
118
+ }
119
+ async *_streamResponseChunks(messages, options, runManager) {
120
+ const reasoningMap = (options == null ? void 0 : options[REASONING_MAP_KEY]) instanceof Map ? options[REASONING_MAP_KEY] : collectReasoningMap(messages);
121
+ yield* super._streamResponseChunks(messages, { ...options || {}, [REASONING_MAP_KEY]: reasoningMap }, runManager);
122
+ }
123
+ _convertCompletionsDeltaToBaseMessageChunk(delta, rawResponse, defaultRole) {
124
+ const messageChunk = super._convertCompletionsDeltaToBaseMessageChunk(delta, rawResponse, defaultRole);
125
+ if (delta == null ? void 0 : delta.reasoning_content) {
126
+ messageChunk.additional_kwargs = {
127
+ ...messageChunk.additional_kwargs || {},
128
+ reasoning_content: delta.reasoning_content
129
+ };
130
+ }
131
+ return messageChunk;
132
+ }
133
+ _convertCompletionsMessageToBaseMessage(message, rawResponse) {
134
+ const langChainMessage = super._convertCompletionsMessageToBaseMessage(message, rawResponse);
135
+ if (message == null ? void 0 : message.reasoning_content) {
136
+ langChainMessage.additional_kwargs = {
137
+ ...langChainMessage.additional_kwargs || {},
138
+ reasoning_content: message.reasoning_content
139
+ };
140
+ }
141
+ return langChainMessage;
142
+ }
143
+ async completionWithRetry(request, requestOptions) {
144
+ const reasoningMap = requestOptions == null ? void 0 : requestOptions[REASONING_MAP_KEY];
145
+ patchRequestMessagesReasoning(request, reasoningMap);
146
+ return super.completionWithRetry(request, requestOptions);
147
+ }
148
+ };
149
+ }
61
150
  let _ChatGenerationChunk = null;
62
151
  function getChatGenerationChunk() {
63
152
  if (!_ChatGenerationChunk) {
@@ -150,12 +239,25 @@ function getByPath(obj, dotPath) {
150
239
  }
151
240
  return current;
152
241
  }
153
- function createMappingFetch(responseMapping) {
242
+ function createMappingFetch(responseMapping, timeoutMs) {
154
243
  const contentPath = responseMapping.content;
155
244
  if (!contentPath) return void 0;
245
+ const toolCallsPath = responseMapping.tool_calls;
246
+ const finishReasonPath = responseMapping.finish_reason;
156
247
  return async (url, init) => {
157
248
  var _a, _b;
158
- const response = await fetch(url, init);
249
+ let response;
250
+ if (timeoutMs && timeoutMs > 0) {
251
+ const controller = new AbortController();
252
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
253
+ try {
254
+ response = await fetch(url, { ...init, signal: controller.signal });
255
+ } finally {
256
+ clearTimeout(timer);
257
+ }
258
+ } else {
259
+ response = await fetch(url, init);
260
+ }
159
261
  if (!response.ok) return response;
160
262
  const contentType = response.headers.get("content-type") || "";
161
263
  if (contentType.includes("text/event-stream") || ((_a = init == null ? void 0 : init.headers) == null ? void 0 : _a["Accept"]) === "text/event-stream") {
@@ -186,17 +288,28 @@ function createMappingFetch(responseMapping) {
186
288
  try {
187
289
  const parsed = JSON.parse(data);
188
290
  const mappedContent = getByPath(parsed, contentPath);
189
- if (mappedContent !== void 0) {
291
+ const mappedToolCalls = toolCallsPath ? getByPath(parsed, toolCallsPath) : getByPath(parsed, "choices.0.delta.tool_calls") ?? getByPath(parsed, "delta.tool_calls");
292
+ const mappedFinishReason = finishReasonPath ? getByPath(parsed, finishReasonPath) : getByPath(parsed, "choices.0.finish_reason") ?? getByPath(parsed, "finish_reason");
293
+ if (mappedContent !== void 0 || mappedToolCalls) {
294
+ const delta = { role: "assistant" };
295
+ if (mappedContent !== void 0) {
296
+ delta.content = String(mappedContent);
297
+ }
298
+ if (mappedToolCalls) {
299
+ delta.tool_calls = mappedToolCalls;
300
+ }
190
301
  const mapped = {
191
302
  id: getByPath(parsed, responseMapping.id || "id") || "chatcmpl-custom",
192
303
  object: "chat.completion.chunk",
193
304
  created: Math.floor(Date.now() / 1e3),
194
305
  model: "custom",
195
- choices: [{
196
- index: 0,
197
- delta: { content: String(mappedContent), role: "assistant" },
198
- finish_reason: null
199
- }]
306
+ choices: [
307
+ {
308
+ index: 0,
309
+ delta,
310
+ finish_reason: mappedFinishReason ?? null
311
+ }
312
+ ]
200
313
  };
201
314
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(mapped)}
202
315
 
@@ -228,21 +341,33 @@ function createMappingFetch(responseMapping) {
228
341
  if (contentType.includes("application/json")) {
229
342
  const body = await response.json();
230
343
  const mappedContent = getByPath(body, contentPath);
231
- if (mappedContent !== void 0) {
344
+ const mappedToolCalls = toolCallsPath ? getByPath(body, toolCallsPath) : getByPath(body, "choices.0.message.tool_calls") ?? getByPath(body, "message.tool_calls");
345
+ const mappedFinishReason = finishReasonPath ? getByPath(body, finishReasonPath) : getByPath(body, "choices.0.finish_reason") ?? getByPath(body, "finish_reason");
346
+ if (mappedContent !== void 0 || mappedToolCalls) {
347
+ const message = {
348
+ role: getByPath(body, responseMapping.role || "") || "assistant"
349
+ };
350
+ if (mappedContent !== void 0) {
351
+ message.content = String(mappedContent);
352
+ } else {
353
+ message.content = null;
354
+ }
355
+ if (mappedToolCalls) {
356
+ message.tool_calls = mappedToolCalls;
357
+ }
232
358
  const mapped = {
233
359
  id: getByPath(body, responseMapping.id || "id") || "chatcmpl-custom",
234
360
  object: "chat.completion",
235
361
  created: Math.floor(Date.now() / 1e3),
236
362
  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 }
363
+ choices: [
364
+ {
365
+ index: 0,
366
+ message,
367
+ finish_reason: mappedFinishReason ?? (mappedToolCalls ? "tool_calls" : "stop")
368
+ }
369
+ ],
370
+ usage: body.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
246
371
  };
247
372
  return new Response(JSON.stringify(mapped), {
248
373
  status: response.status,
@@ -417,6 +542,45 @@ function fixEmptyToolProperties(model) {
417
542
  };
418
543
  return model;
419
544
  }
545
+ function wrapWithToolCallIdSanitizer(model) {
546
+ var _a, _b;
547
+ const originalGenerate = (_a = model._generate) == null ? void 0 : _a.bind(model);
548
+ if (originalGenerate) {
549
+ model._generate = async function(...args) {
550
+ const result = await originalGenerate(...args);
551
+ for (const gen of (result == null ? void 0 : result.generations) ?? []) {
552
+ const msg = gen == null ? void 0 : gen.message;
553
+ if (msg == null ? void 0 : msg.tool_calls) {
554
+ for (const tc of msg.tool_calls) {
555
+ tc.id = sanitizeToolCallId(tc.id);
556
+ }
557
+ }
558
+ }
559
+ return result;
560
+ };
561
+ }
562
+ const streamMethod = typeof model._streamResponseChunks === "function" ? "_streamResponseChunks" : "_stream";
563
+ const originalStream = (_b = model[streamMethod]) == null ? void 0 : _b.bind(model);
564
+ if (originalStream) {
565
+ model[streamMethod] = async function* (...args) {
566
+ for await (const chunk of originalStream(...args)) {
567
+ const msg = chunk == null ? void 0 : chunk.message;
568
+ if (msg == null ? void 0 : msg.tool_call_chunks) {
569
+ for (const tc of msg.tool_call_chunks) {
570
+ tc.id = sanitizeToolCallId(tc.id);
571
+ }
572
+ }
573
+ if (msg == null ? void 0 : msg.tool_calls) {
574
+ for (const tc of msg.tool_calls) {
575
+ tc.id = sanitizeToolCallId(tc.id);
576
+ }
577
+ }
578
+ yield chunk;
579
+ }
580
+ };
581
+ }
582
+ return model;
583
+ }
420
584
  class CustomLLMProvider extends import_plugin_ai.LLMProvider {
421
585
  get baseURL() {
422
586
  return null;
@@ -431,7 +595,15 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
431
595
  }
432
596
  createModel() {
433
597
  var _a;
434
- const { apiKey, disableStream, timeout, streamKeepAlive, keepAliveIntervalMs, keepAliveContent } = this.serviceOptions || {};
598
+ const {
599
+ apiKey,
600
+ disableStream,
601
+ timeout,
602
+ streamKeepAlive,
603
+ keepAliveIntervalMs,
604
+ keepAliveContent,
605
+ enableReasoning
606
+ } = this.serviceOptions || {};
435
607
  const baseURL = (_a = this.serviceOptions) == null ? void 0 : _a.baseURL;
436
608
  const { responseFormat } = this.modelOptions || {};
437
609
  const reqConfig = this.requestConfig;
@@ -446,7 +618,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
446
618
  if (reqConfig.extraBody && typeof reqConfig.extraBody === "object") {
447
619
  Object.assign(modelKwargs, reqConfig.extraBody);
448
620
  }
449
- const ChatOpenAI = getChatOpenAI();
621
+ const ChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
450
622
  const config = {
451
623
  apiKey,
452
624
  ...this.modelOptions,
@@ -459,18 +631,20 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
459
631
  if (disableStream) {
460
632
  config.streaming = false;
461
633
  }
462
- if (timeout && Number(timeout) > 0) {
463
- config.timeout = Number(timeout);
464
- config.configuration.timeout = Number(timeout);
634
+ const timeoutMs = timeout && Number(timeout) > 0 ? Number(timeout) : 0;
635
+ if (timeoutMs) {
636
+ config.timeout = timeoutMs;
637
+ config.configuration.timeout = timeoutMs;
465
638
  }
466
639
  if (reqConfig.extraHeaders && typeof reqConfig.extraHeaders === "object") {
467
640
  config.configuration.defaultHeaders = reqConfig.extraHeaders;
468
641
  }
469
642
  if (resConfig.responseMapping) {
470
- config.configuration.fetch = createMappingFetch(resConfig.responseMapping);
643
+ config.configuration.fetch = createMappingFetch(resConfig.responseMapping, timeoutMs || 12e4);
471
644
  }
472
- let model = new ChatOpenAI(config);
645
+ let model = new ChatClass(config);
473
646
  model = fixEmptyToolProperties(model);
647
+ model = wrapWithToolCallIdSanitizer(model);
474
648
  if (streamKeepAlive && !disableStream) {
475
649
  return wrapWithStreamKeepAlive(model, {
476
650
  intervalMs: Number(keepAliveIntervalMs) || 5e3,
@@ -498,15 +672,14 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
498
672
  workContext
499
673
  };
500
674
  if (toolCalls) {
501
- content.tool_calls = toolCalls;
675
+ content.tool_calls = Array.isArray(toolCalls) ? toolCalls.map((tc) => ({ ...tc, id: sanitizeToolCallId(tc.id) })) : toolCalls;
502
676
  }
503
677
  if (Array.isArray(content.content)) {
504
678
  const textBlocks = content.content.filter((block) => block.type === "text");
505
679
  content.content = textBlocks.map((block) => block.text).join("") || "";
506
680
  }
507
681
  if (typeof content.content === "string") {
508
- const escapedPrefix = KEEPALIVE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
509
- content.content = content.content.replace(new RegExp(escapedPrefix + ".*?(?=" + escapedPrefix + "|$)", "g"), "");
682
+ content.content = content.content.replaceAll(KEEPALIVE_PREFIX, "");
510
683
  content.content = stripToolCallTags(content.content);
511
684
  }
512
685
  if (((_b = (_a = content.metadata) == null ? void 0 : _a.additional_kwargs) == null ? void 0 : _b.__keepalive) !== void 0) {
@@ -640,10 +813,10 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
640
813
  * from DB. Result is cached on `ctx.state._docPixieActive` for the request lifetime.
641
814
  */
642
815
  async hasDocPixieSkill(ctx) {
643
- var _a, _b, _c, _d, _e;
816
+ var _a, _b, _c, _d, _e, _f, _g, _h;
644
817
  if (ctx.state._docPixieActive !== void 0) return ctx.state._docPixieActive;
645
818
  try {
646
- const employeeUsername = (_c = (_b = (_a = ctx.action) == null ? void 0 : _a.params) == null ? void 0 : _b.values) == null ? void 0 : _c.aiEmployee;
819
+ 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
820
  if (!employeeUsername) {
648
821
  ctx.state._docPixieActive = false;
649
822
  return false;
@@ -652,7 +825,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
652
825
  filter: { username: String(employeeUsername) },
653
826
  fields: ["skillSettings"]
654
827
  });
655
- const skills = ((_e = (_d = employee == null ? void 0 : employee.get) == null ? void 0 : _d.call(employee, "skillSettings")) == null ? void 0 : _e.skills) ?? [];
828
+ const skills = ((_h = (_g = employee == null ? void 0 : employee.get) == null ? void 0 : _g.call(employee, "skillSettings")) == null ? void 0 : _h.skills) ?? [];
656
829
  const has = skills.some((s) => s.name === "docpixie.query.document");
657
830
  ctx.state._docPixieActive = has;
658
831
  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.3",
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.1",
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;