plugin-custom-llm 1.1.1 → 1.2.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 +28 -1
- package/dist/externalVersion.js +6 -6
- package/dist/server/llm-providers/custom-llm.js +99 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ NocoBase plugin for integrating external LLM providers that support OpenAI-compa
|
|
|
7
7
|
- **OpenAI-compatible**: Works with any LLM provider exposing `/chat/completions` endpoint
|
|
8
8
|
- **Auto content detection**: Handles both string and array content blocks (`[{type: 'text', text: '...'}]`)
|
|
9
9
|
- **Response mapping**: Transform non-standard API responses to OpenAI format via JSON config (supports streaming SSE and JSON)
|
|
10
|
-
- **Reasoning content**: Display thinking/reasoning from DeepSeek-compatible providers
|
|
10
|
+
- **Reasoning content**: Display thinking/reasoning from DeepSeek-compatible providers (multi-path detection)
|
|
11
|
+
- **Stream keepalive**: Prevent proxy/gateway timeouts during long model thinking phases
|
|
12
|
+
- **Tool calling support**: Gemini-compatible tool schema fixing (Zod + JSON Schema)
|
|
11
13
|
- **Configurable**: JSON config editors for request and response customization
|
|
12
14
|
- **Locale support**: English, Vietnamese, Chinese
|
|
13
15
|
|
|
@@ -23,6 +25,11 @@ Upload `plugin-custom-llm-x.x.x.tgz` via NocoBase Plugin Manager UI, then enable
|
|
|
23
25
|
|---|---|
|
|
24
26
|
| **Base URL** | LLM endpoint URL, e.g. `https://your-llm-server.com/v1` |
|
|
25
27
|
| **API Key** | Authentication key |
|
|
28
|
+
| **Disable Streaming** | Disable streaming for models that return empty stream values |
|
|
29
|
+
| **Stream Keep Alive** | Enable keepalive to prevent timeouts during long thinking phases |
|
|
30
|
+
| **Keep Alive Interval** | Interval in ms between keepalive signals (default: 5000) |
|
|
31
|
+
| **Keep Alive Content** | Visual indicator text during keepalive (default: `...`) |
|
|
32
|
+
| **Timeout** | Custom timeout in ms for slow-responding models |
|
|
26
33
|
| **Request config (JSON)** | Optional. Extra request configuration |
|
|
27
34
|
| **Response config (JSON)** | Optional. Response parsing and mapping configuration |
|
|
28
35
|
|
|
@@ -72,6 +79,26 @@ Upload `plugin-custom-llm-x.x.x.tgz` via NocoBase Plugin Manager UI, then enable
|
|
|
72
79
|
|
|
73
80
|
Standard OpenAI-compatible parameters: temperature, max tokens, top P, frequency/presence penalty, response format, timeout, max retries.
|
|
74
81
|
|
|
82
|
+
## Changelog
|
|
83
|
+
|
|
84
|
+
### v1.2.0
|
|
85
|
+
|
|
86
|
+
- **Fix**: Keepalive no longer interferes with tool call sequences (prevents tool call corruption)
|
|
87
|
+
- **Fix**: Gemini-compatible tool schema fixing — handles Zod schemas via dual-phase approach (pre/post conversion)
|
|
88
|
+
- **Fix**: Keepalive content no longer contaminates saved messages in DB
|
|
89
|
+
- **Fix**: Response metadata extraction with long ID sanitization (>128 chars truncated)
|
|
90
|
+
- **Fix**: Multi-path reasoning content detection (`additional_kwargs` + `kwargs.additional_kwargs`)
|
|
91
|
+
- **Fix**: Improved error recovery in keepalive consumer (immediate error propagation)
|
|
92
|
+
|
|
93
|
+
### v1.1.1
|
|
94
|
+
|
|
95
|
+
- Stream keepalive proxy for long thinking phases
|
|
96
|
+
- Response mapping for non-standard LLM APIs
|
|
97
|
+
|
|
98
|
+
### v1.0.0
|
|
99
|
+
|
|
100
|
+
- Initial release with OpenAI-compatible LLM provider support
|
|
101
|
+
|
|
75
102
|
## License
|
|
76
103
|
|
|
77
104
|
Apache-2.0
|
package/dist/externalVersion.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
|
-
"@nocobase/client": "2.0.
|
|
12
|
-
"@nocobase/plugin-ai": "2.0.
|
|
11
|
+
"@nocobase/client": "2.0.20",
|
|
12
|
+
"@nocobase/plugin-ai": "2.0.20",
|
|
13
13
|
"react-i18next": "11.18.6",
|
|
14
|
-
"@nocobase/server": "2.0.
|
|
15
|
-
"@nocobase/flow-engine": "2.0.
|
|
16
|
-
"@nocobase/database": "2.0.
|
|
14
|
+
"@nocobase/server": "2.0.20",
|
|
15
|
+
"@nocobase/flow-engine": "2.0.20",
|
|
16
|
+
"@nocobase/database": "2.0.20",
|
|
17
17
|
"react": "18.2.0",
|
|
18
|
-
"@nocobase/utils": "2.0.
|
|
18
|
+
"@nocobase/utils": "2.0.20",
|
|
19
19
|
"antd": "5.24.2"
|
|
20
20
|
};
|
|
@@ -236,9 +236,16 @@ function wrapWithStreamKeepAlive(model, options) {
|
|
|
236
236
|
let streamDone = false;
|
|
237
237
|
let streamError = null;
|
|
238
238
|
let notifyReady = null;
|
|
239
|
+
let hasToolCallChunks = false;
|
|
240
|
+
let hasErrored = false;
|
|
239
241
|
const consumer = (async () => {
|
|
242
|
+
var _a, _b;
|
|
240
243
|
try {
|
|
241
244
|
for await (const chunk of baseIterator) {
|
|
245
|
+
const msg = chunk == null ? void 0 : chunk.message;
|
|
246
|
+
if (((_a = msg == null ? void 0 : msg.tool_call_chunks) == null ? void 0 : _a.length) || ((_b = msg == null ? void 0 : msg.tool_calls) == null ? void 0 : _b.length)) {
|
|
247
|
+
hasToolCallChunks = true;
|
|
248
|
+
}
|
|
242
249
|
buffer.push(chunk);
|
|
243
250
|
if (notifyReady) {
|
|
244
251
|
notifyReady();
|
|
@@ -247,6 +254,11 @@ function wrapWithStreamKeepAlive(model, options) {
|
|
|
247
254
|
}
|
|
248
255
|
} catch (err) {
|
|
249
256
|
streamError = err;
|
|
257
|
+
hasErrored = true;
|
|
258
|
+
if (notifyReady) {
|
|
259
|
+
notifyReady();
|
|
260
|
+
notifyReady = null;
|
|
261
|
+
}
|
|
250
262
|
} finally {
|
|
251
263
|
streamDone = true;
|
|
252
264
|
if (notifyReady) {
|
|
@@ -260,6 +272,7 @@ function wrapWithStreamKeepAlive(model, options) {
|
|
|
260
272
|
while (buffer.length > 0) {
|
|
261
273
|
yield buffer.shift();
|
|
262
274
|
}
|
|
275
|
+
hasToolCallChunks = false;
|
|
263
276
|
if (streamDone) break;
|
|
264
277
|
const waitForChunk = new Promise((resolve) => {
|
|
265
278
|
notifyReady = resolve;
|
|
@@ -273,9 +286,14 @@ function wrapWithStreamKeepAlive(model, options) {
|
|
|
273
286
|
]);
|
|
274
287
|
if (timer) clearTimeout(timer);
|
|
275
288
|
if (result === "timeout" && !streamDone && buffer.length === 0) {
|
|
289
|
+
if (streamError || hasErrored) break;
|
|
290
|
+
if (hasToolCallChunks) continue;
|
|
276
291
|
const keepAliveChunk = new ChatGenerationChunk({
|
|
277
|
-
message: new AIMessageChunk({
|
|
278
|
-
|
|
292
|
+
message: new AIMessageChunk({
|
|
293
|
+
content: KEEPALIVE_PREFIX,
|
|
294
|
+
additional_kwargs: { __keepalive: true }
|
|
295
|
+
}),
|
|
296
|
+
text: KEEPALIVE_PREFIX
|
|
279
297
|
});
|
|
280
298
|
yield keepAliveChunk;
|
|
281
299
|
}
|
|
@@ -296,37 +314,51 @@ function fixEmptyToolProperties(model) {
|
|
|
296
314
|
var _a;
|
|
297
315
|
const originalBind = (_a = model.bindTools) == null ? void 0 : _a.bind(model);
|
|
298
316
|
if (!originalBind) return model;
|
|
317
|
+
const PLACEHOLDER_PROP = {
|
|
318
|
+
_placeholder: { type: "string", description: "No parameters required" }
|
|
319
|
+
};
|
|
320
|
+
function fixPropertiesInSchema(schema) {
|
|
321
|
+
if (!schema || typeof schema !== "object") return;
|
|
322
|
+
if (schema.properties && typeof schema.properties === "object" && Object.keys(schema.properties).length === 0) {
|
|
323
|
+
schema.properties = { ...PLACEHOLDER_PROP };
|
|
324
|
+
}
|
|
325
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
326
|
+
if (Array.isArray(schema[key])) {
|
|
327
|
+
schema[key].forEach((sub) => fixPropertiesInSchema(sub));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
299
331
|
model.bindTools = function(tools, kwargs) {
|
|
332
|
+
var _a2;
|
|
300
333
|
const fixedTools = tools.map((tool) => {
|
|
301
|
-
var
|
|
334
|
+
var _a3, _b;
|
|
302
335
|
if (!tool || typeof tool !== "object") return tool;
|
|
336
|
+
if (typeof ((_a3 = tool.schema) == null ? void 0 : _a3.safeParse) === "function") {
|
|
337
|
+
return tool;
|
|
338
|
+
}
|
|
303
339
|
const schema = tool.schema;
|
|
304
|
-
if (schema && typeof schema === "object") {
|
|
305
|
-
const props = schema.properties
|
|
340
|
+
if (schema && typeof schema === "object" && !schema.safeParse) {
|
|
341
|
+
const props = schema.properties;
|
|
306
342
|
if (props && typeof props === "object" && Object.keys(props).length === 0) {
|
|
307
343
|
return {
|
|
308
344
|
...tool,
|
|
309
345
|
schema: {
|
|
310
346
|
...schema,
|
|
311
|
-
properties: {
|
|
312
|
-
_placeholder: { type: "string", description: "No parameters required" }
|
|
313
|
-
}
|
|
347
|
+
properties: { ...PLACEHOLDER_PROP }
|
|
314
348
|
}
|
|
315
349
|
};
|
|
316
350
|
}
|
|
317
351
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (typeof
|
|
352
|
+
const funcParams = (_b = tool.function) == null ? void 0 : _b.parameters;
|
|
353
|
+
if (funcParams == null ? void 0 : funcParams.properties) {
|
|
354
|
+
if (typeof funcParams.properties === "object" && Object.keys(funcParams.properties).length === 0) {
|
|
321
355
|
return {
|
|
322
356
|
...tool,
|
|
323
357
|
function: {
|
|
324
358
|
...tool.function,
|
|
325
359
|
parameters: {
|
|
326
|
-
...
|
|
327
|
-
properties: {
|
|
328
|
-
_placeholder: { type: "string", description: "No parameters required" }
|
|
329
|
-
}
|
|
360
|
+
...funcParams,
|
|
361
|
+
properties: { ...PLACEHOLDER_PROP }
|
|
330
362
|
}
|
|
331
363
|
}
|
|
332
364
|
};
|
|
@@ -334,7 +366,22 @@ function fixEmptyToolProperties(model) {
|
|
|
334
366
|
}
|
|
335
367
|
return tool;
|
|
336
368
|
});
|
|
337
|
-
|
|
369
|
+
const result = originalBind(fixedTools, kwargs);
|
|
370
|
+
try {
|
|
371
|
+
const config = (result == null ? void 0 : result.kwargs) ?? (result == null ? void 0 : result.defaultOptions);
|
|
372
|
+
if ((config == null ? void 0 : config.tools) && Array.isArray(config.tools)) {
|
|
373
|
+
for (const tool of config.tools) {
|
|
374
|
+
if ((_a2 = tool == null ? void 0 : tool.function) == null ? void 0 : _a2.parameters) {
|
|
375
|
+
fixPropertiesInSchema(tool.function.parameters);
|
|
376
|
+
}
|
|
377
|
+
if (tool == null ? void 0 : tool.parameters) {
|
|
378
|
+
fixPropertiesInSchema(tool.parameters);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
338
385
|
};
|
|
339
386
|
return model;
|
|
340
387
|
}
|
|
@@ -404,11 +451,12 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
|
404
451
|
const resConfig = this.responseConfig;
|
|
405
452
|
const text = extractTextContent(chunk, resConfig.contentPath);
|
|
406
453
|
if (isKeepAlive(text)) {
|
|
407
|
-
return
|
|
454
|
+
return KEEPALIVE_PREFIX;
|
|
408
455
|
}
|
|
409
456
|
return stripToolCallTags(text);
|
|
410
457
|
}
|
|
411
458
|
parseResponseMessage(message) {
|
|
459
|
+
var _a, _b;
|
|
412
460
|
const { content: rawContent, messageId, metadata, role, toolCalls, attachments, workContext } = message;
|
|
413
461
|
const content = {
|
|
414
462
|
...rawContent ?? {},
|
|
@@ -429,6 +477,10 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
|
429
477
|
content.content = content.content.replace(new RegExp(escapedPrefix + ".*?(?=" + escapedPrefix + "|$)", "g"), "");
|
|
430
478
|
content.content = stripToolCallTags(content.content);
|
|
431
479
|
}
|
|
480
|
+
if (((_b = (_a = content.metadata) == null ? void 0 : _a.additional_kwargs) == null ? void 0 : _b.__keepalive) !== void 0) {
|
|
481
|
+
const { __keepalive, ...cleanKwargs } = content.metadata.additional_kwargs;
|
|
482
|
+
content.metadata = { ...content.metadata, additional_kwargs: cleanKwargs };
|
|
483
|
+
}
|
|
432
484
|
return {
|
|
433
485
|
key: messageId,
|
|
434
486
|
content,
|
|
@@ -436,15 +488,43 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
|
436
488
|
};
|
|
437
489
|
}
|
|
438
490
|
parseReasoningContent(chunk) {
|
|
439
|
-
var _a;
|
|
491
|
+
var _a, _b, _c;
|
|
440
492
|
const resConfig = this.responseConfig;
|
|
441
493
|
const reasoningKey = resConfig.reasoningKey || "reasoning_content";
|
|
442
|
-
const reasoning = (_a = chunk == null ? void 0 : chunk.additional_kwargs) == null ? void 0 : _a[reasoningKey];
|
|
494
|
+
const reasoning = ((_a = chunk == null ? void 0 : chunk.additional_kwargs) == null ? void 0 : _a[reasoningKey]) ?? ((_c = (_b = chunk == null ? void 0 : chunk.kwargs) == null ? void 0 : _b.additional_kwargs) == null ? void 0 : _c[reasoningKey]);
|
|
443
495
|
if (reasoning && typeof reasoning === "string") {
|
|
444
496
|
return { status: "streaming", content: reasoning };
|
|
445
497
|
}
|
|
446
498
|
return null;
|
|
447
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Extract response metadata from LLM output for post-save enrichment.
|
|
502
|
+
* Sanitizes overly long message IDs from Gemini or other providers.
|
|
503
|
+
*/
|
|
504
|
+
parseResponseMetadata(output) {
|
|
505
|
+
var _a, _b;
|
|
506
|
+
try {
|
|
507
|
+
const generation = (_b = (_a = output == null ? void 0 : output.generations) == null ? void 0 : _a[0]) == null ? void 0 : _b[0];
|
|
508
|
+
if (!generation) return [null, null];
|
|
509
|
+
const message = generation.message;
|
|
510
|
+
let id = message == null ? void 0 : message.id;
|
|
511
|
+
if (!id) return [null, null];
|
|
512
|
+
if (typeof id === "string" && id.length > 128) {
|
|
513
|
+
id = id.substring(0, 128);
|
|
514
|
+
}
|
|
515
|
+
const metadata = {};
|
|
516
|
+
if (message == null ? void 0 : message.response_metadata) {
|
|
517
|
+
metadata.finish_reason = message.response_metadata.finish_reason;
|
|
518
|
+
metadata.system_fingerprint = message.response_metadata.system_fingerprint;
|
|
519
|
+
}
|
|
520
|
+
if (message == null ? void 0 : message.usage_metadata) {
|
|
521
|
+
metadata.usage_metadata = message.usage_metadata;
|
|
522
|
+
}
|
|
523
|
+
return Object.keys(metadata).length > 0 ? [id, metadata] : [null, null];
|
|
524
|
+
} catch {
|
|
525
|
+
return [null, null];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
448
528
|
parseResponseError(err) {
|
|
449
529
|
return (err == null ? void 0 : err.message) ?? "Unexpected LLM service error";
|
|
450
530
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "AI LLM: Custom (OpenAI Compatible)",
|
|
4
4
|
"displayName.zh-CN": "AI LLM:自定义(OpenAI 兼容)",
|
|
5
5
|
"description": "OpenAI-compatible LLM provider with auto response format detection for external LLM services.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.2.0",
|
|
7
7
|
"main": "dist/server/index.js",
|
|
8
8
|
"nocobase": {
|
|
9
9
|
"supportedVersions": [
|