plugin-custom-llm 1.1.0 → 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 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
@@ -7,4 +7,4 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):"function"==typeof define&&define.amd?define("plugin-custom-llm",["react","@nocobase/plugin-ai/client","@nocobase/client","@nocobase/utils/client","antd","react-i18next"],t):"object"==typeof exports?exports["plugin-custom-llm"]=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):e["plugin-custom-llm"]=t(e.react,e["@nocobase/plugin-ai/client"],e["@nocobase/client"],e["@nocobase/utils/client"],e.antd,e["react-i18next"])}(self,function(e,t,n,o,r,i){return function(){"use strict";var a={772:function(e){e.exports=n},645:function(e){e.exports=t},584:function(e){e.exports=o},721:function(e){e.exports=r},156:function(t){t.exports=e},238:function(e){e.exports=i}},c={};function l(e){var t=c[e];if(void 0!==t)return t.exports;var n=c[e]={exports:{}};return a[e](n,n.exports,l),n.exports}l.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(t,{a:t}),t},l.d=function(e,t){for(var n in t)l.o(t,n)&&!l.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},l.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var u={};return!function(){l.r(u),l.d(u,{PluginCustomLLMClient:function(){return g},default:function(){return S}});var e=l(772),t=l(156),n=l.n(t),o=l(584),r=l(238),i="@nocobase/plugin-custom-llm",a=l(721),c=l(645),p=function(){var t=(0,r.useTranslation)(i,{nsMode:"fallback"}).t;return n().createElement("div",{style:{marginBottom:24}},n().createElement(a.Collapse,{bordered:!1,size:"small",items:[{key:"options",label:t("Options"),forceRender:!0,children:n().createElement(e.SchemaComponent,{schema:{type:"void",name:"custom-llm",properties:{temperature:{title:(0,o.tval)("Temperature",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:.7,"x-component-props":{step:.1,min:0,max:2}},maxCompletionTokens:{title:(0,o.tval)("Max completion tokens",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:-1},topP:{title:(0,o.tval)("Top P",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1,"x-component-props":{step:.1,min:0,max:1}},frequencyPenalty:{title:(0,o.tval)("Frequency penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},presencePenalty:{title:(0,o.tval)("Presence penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},responseFormat:{title:(0,o.tval)("Response format",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Select",enum:[{label:t("Text"),value:"text"},{label:t("JSON"),value:"json_object"}],default:"text"},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:6e4},maxRetries:{title:(0,o.tval)("Max retries",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1}}}})}]}))},s={components:{ProviderSettingsForm:function(){return n().createElement(e.SchemaComponent,{schema:{type:"void",properties:{baseURL:{title:(0,o.tval)("Base URL",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"TextAreaWithGlobalScope","x-component-props":{placeholder:"https://your-llm-server.com/v1"}},apiKey:{title:(0,o.tval)("API Key",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"TextAreaWithGlobalScope"},disableStream:{title:(0,o.tval)("Disable streaming",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Disable streaming description",{ns:i})},streamKeepAlive:{title:(0,o.tval)("Stream keepalive",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Stream keepalive description",{ns:i})},keepAliveIntervalMs:{title:(0,o.tval)("Keepalive interval (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"5000",min:1e3,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Keepalive interval description",{ns:i})},keepAliveContent:{title:(0,o.tval)("Keepalive content",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input","x-component-props":{placeholder:"..."},description:(0,o.tval)("Keepalive content description",{ns:i})},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"120000",min:0,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Timeout description",{ns:i})},requestConfig:{title:(0,o.tval)("Request config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({extraHeaders:{},extraBody:{},modelKwargs:{}},null,2),rows:6,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Request config description",{ns:i})},responseConfig:{title:(0,o.tval)("Response config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({contentPath:"auto",reasoningKey:"reasoning_content",responseMapping:{content:"message.response"}},null,2),rows:8,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Response config description",{ns:i})}}}})},ModelSettingsForm:function(){return n().createElement(e.SchemaComponent,{components:{Options:p,ModelSelect:c.ModelSelect},schema:{type:"void",properties:{model:{title:(0,o.tval)("Model",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"ModelSelect"},options:{type:"void","x-component":"Options"}}}})}}};function m(e,t,n,o,r,i,a){try{var c=e[i](a),l=c.value}catch(e){n(e);return}c.done?t(l):Promise.resolve(l).then(o,r)}function f(e){return function(){var t=this,n=arguments;return new Promise(function(o,r){var i=e.apply(t,n);function a(e){m(i,o,r,a,c,"next",e)}function c(e){m(i,o,r,a,c,"throw",e)}a(void 0)})}}function d(e,t,n){return(d=x()?Reflect.construct:function(e,t,n){var o=[null];o.push.apply(o,t);var r=new(Function.bind.apply(e,o));return n&&b(r,n.prototype),r}).apply(null,arguments)}function y(e){return(y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function b(e,t){return(b=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function v(e){var t="function"==typeof Map?new Map:void 0;return(v=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return d(e,arguments,y(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),b(n,e)})(e)}function x(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(x=function(){return!!e})()}function h(e,t){var n,o,r,i,a={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return i={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function c(i){return function(c){var l=[i,c];if(n)throw TypeError("Generator is already executing.");for(;a;)try{if(n=1,o&&(r=2&l[0]?o.return:l[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,l[1])).done)return r;switch(o=0,r&&(l=[2&l[0],r.value]),l[0]){case 0:case 1:r=l;break;case 4:return a.label++,{value:l[1],done:!1};case 5:a.label++,o=l[1],l=[0];continue;case 7:l=a.ops.pop(),a.trys.pop();continue;default:if(!(r=(r=a.trys).length>0&&r[r.length-1])&&(6===l[0]||2===l[0])){a=0;continue}if(3===l[0]&&(!r||l[1]>r[0]&&l[1]<r[3])){a.label=l[1];break}if(6===l[0]&&a.label<r[1]){a.label=r[1],r=l;break}if(r&&a.label<r[2]){a.label=r[2],a.ops.push(l);break}r[2]&&a.ops.pop(),a.trys.pop();continue}l=t.call(e,a)}catch(e){l=[6,e],o=0}finally{n=r=0}if(5&l[0])throw l[1];return{value:l[0]?l[1]:void 0,done:!0}}}}var g=function(e){var t;if("function"!=typeof e&&null!==e)throw TypeError("Super expression must either be null or a function");function n(){var e,t;if(!(this instanceof n))throw TypeError("Cannot call a class as a function");return e=n,t=arguments,e=y(e),function(e,t){var n;if(t&&("object"==((n=t)&&"undefined"!=typeof Symbol&&n.constructor===Symbol?"symbol":typeof n)||"function"==typeof t))return t;if(void 0===e)throw ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(this,x()?Reflect.construct(e,t||[],y(this).constructor):e.apply(this,t))}return n.prototype=Object.create(e&&e.prototype,{constructor:{value:n,writable:!0,configurable:!0}}),e&&b(n,e),t=[{key:"afterAdd",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"beforeLoad",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"load",value:function(){var e=this;return f(function(){return h(this,function(t){return e.aiPlugin.aiManager.registerLLMProvider("custom-llm",s),[2]})})()}},{key:"aiPlugin",get:function(){return this.app.pm.get("ai")}}],function(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}(n.prototype,t),n}(v(e.Plugin)),S=g}(),u}()});
10
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):"function"==typeof define&&define.amd?define("plugin-custom-llm",["react","@nocobase/plugin-ai/client","@nocobase/client","@nocobase/utils/client","antd","react-i18next"],t):"object"==typeof exports?exports["plugin-custom-llm"]=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):e["plugin-custom-llm"]=t(e.react,e["@nocobase/plugin-ai/client"],e["@nocobase/client"],e["@nocobase/utils/client"],e.antd,e["react-i18next"])}(self,function(e,t,n,o,r,i){return function(){"use strict";var a={772:function(e){e.exports=n},645:function(e){e.exports=t},584:function(e){e.exports=o},721:function(e){e.exports=r},156:function(t){t.exports=e},238:function(e){e.exports=i}},c={};function u(e){var t=c[e];if(void 0!==t)return t.exports;var n=c[e]={exports:{}};return a[e](n,n.exports,u),n.exports}u.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return u.d(t,{a:t}),t},u.d=function(e,t){for(var n in t)u.o(t,n)&&!u.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},u.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},u.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var l={};return!function(){u.r(l),u.d(l,{PluginCustomLLMClient:function(){return g},default:function(){return S}});var e=u(772),t=u(156),n=u.n(t),o=u(584),r=u(238),i="@nocobase/plugin-custom-llm",a=u(721),c=u(645),p=function(){var t=(0,r.useTranslation)(i,{nsMode:"fallback"}).t;return n().createElement("div",{style:{marginBottom:24}},n().createElement(a.Collapse,{bordered:!1,size:"small",items:[{key:"options",label:t("Options"),forceRender:!0,children:n().createElement(e.SchemaComponent,{schema:{type:"void",name:"custom-llm",properties:{temperature:{title:(0,o.tval)("Temperature",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:.7,"x-component-props":{step:.1,min:0,max:2}},maxCompletionTokens:{title:(0,o.tval)("Max completion tokens",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:-1},topP:{title:(0,o.tval)("Top P",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1,"x-component-props":{step:.1,min:0,max:1}},frequencyPenalty:{title:(0,o.tval)("Frequency penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},presencePenalty:{title:(0,o.tval)("Presence penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},responseFormat:{title:(0,o.tval)("Response format",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Select",enum:[{label:t("Text"),value:"text"},{label:t("JSON"),value:"json_object"}],default:"text"},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:6e4},maxRetries:{title:(0,o.tval)("Max retries",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1}}}})}]}))},s={components:{ProviderSettingsForm:function(){return n().createElement(e.SchemaComponent,{schema:{type:"void",properties:{apiKey:{title:(0,o.tval)("API Key",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"TextAreaWithGlobalScope"},disableStream:{title:(0,o.tval)("Disable streaming",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Disable streaming description",{ns:i})},streamKeepAlive:{title:(0,o.tval)("Stream keepalive",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Stream keepalive description",{ns:i})},keepAliveIntervalMs:{title:(0,o.tval)("Keepalive interval (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"5000",min:1e3,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Keepalive interval description",{ns:i})},keepAliveContent:{title:(0,o.tval)("Keepalive content",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input","x-component-props":{placeholder:"..."},description:(0,o.tval)("Keepalive content description",{ns:i})},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"120000",min:0,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Timeout description",{ns:i})},requestConfig:{title:(0,o.tval)("Request config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({extraHeaders:{},extraBody:{},modelKwargs:{}},null,2),rows:6,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Request config description",{ns:i})},responseConfig:{title:(0,o.tval)("Response config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({contentPath:"auto",reasoningKey:"reasoning_content",responseMapping:{content:"message.response"}},null,2),rows:8,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Response config description",{ns:i})}}}})},ModelSettingsForm:function(){return n().createElement(e.SchemaComponent,{components:{Options:p,ModelSelect:c.ModelSelect},schema:{type:"void",properties:{model:{title:(0,o.tval)("Model",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"ModelSelect"},options:{type:"void","x-component":"Options"}}}})}}};function m(e,t,n,o,r,i,a){try{var c=e[i](a),u=c.value}catch(e){n(e);return}c.done?t(u):Promise.resolve(u).then(o,r)}function f(e){return function(){var t=this,n=arguments;return new Promise(function(o,r){var i=e.apply(t,n);function a(e){m(i,o,r,a,c,"next",e)}function c(e){m(i,o,r,a,c,"throw",e)}a(void 0)})}}function d(e,t,n){return(d=x()?Reflect.construct:function(e,t,n){var o=[null];o.push.apply(o,t);var r=new(Function.bind.apply(e,o));return n&&b(r,n.prototype),r}).apply(null,arguments)}function y(e){return(y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function b(e,t){return(b=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function v(e){var t="function"==typeof Map?new Map:void 0;return(v=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return d(e,arguments,y(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),b(n,e)})(e)}function x(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(x=function(){return!!e})()}function h(e,t){var n,o,r,i,a={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return i={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function c(i){return function(c){var u=[i,c];if(n)throw TypeError("Generator is already executing.");for(;a;)try{if(n=1,o&&(r=2&u[0]?o.return:u[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,u[1])).done)return r;switch(o=0,r&&(u=[2&u[0],r.value]),u[0]){case 0:case 1:r=u;break;case 4:return a.label++,{value:u[1],done:!1};case 5:a.label++,o=u[1],u=[0];continue;case 7:u=a.ops.pop(),a.trys.pop();continue;default:if(!(r=(r=a.trys).length>0&&r[r.length-1])&&(6===u[0]||2===u[0])){a=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]<r[3])){a.label=u[1];break}if(6===u[0]&&a.label<r[1]){a.label=r[1],r=u;break}if(r&&a.label<r[2]){a.label=r[2],a.ops.push(u);break}r[2]&&a.ops.pop(),a.trys.pop();continue}u=t.call(e,a)}catch(e){u=[6,e],o=0}finally{n=r=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}}}var g=function(e){var t;if("function"!=typeof e&&null!==e)throw TypeError("Super expression must either be null or a function");function n(){var e,t;if(!(this instanceof n))throw TypeError("Cannot call a class as a function");return e=n,t=arguments,e=y(e),function(e,t){var n;if(t&&("object"==((n=t)&&"undefined"!=typeof Symbol&&n.constructor===Symbol?"symbol":typeof n)||"function"==typeof t))return t;if(void 0===e)throw ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(this,x()?Reflect.construct(e,t||[],y(this).constructor):e.apply(this,t))}return n.prototype=Object.create(e&&e.prototype,{constructor:{value:n,writable:!0,configurable:!0}}),e&&b(n,e),t=[{key:"afterAdd",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"beforeLoad",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"load",value:function(){var e=this;return f(function(){return h(this,function(t){return e.aiPlugin.aiManager.registerLLMProvider("custom-llm",s),[2]})})()}},{key:"aiPlugin",get:function(){return this.app.pm.get("ai")}}],function(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}(n.prototype,t),n}(v(e.Plugin)),S=g}(),l}()});
@@ -8,13 +8,13 @@
8
8
  */
9
9
 
10
10
  module.exports = {
11
- "@nocobase/client": "2.0.15",
12
- "@nocobase/plugin-ai": "2.0.15",
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",
15
- "@nocobase/flow-engine": "2.0.15",
16
- "@nocobase/database": "2.0.15",
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.15",
18
+ "@nocobase/utils": "2.0.20",
19
19
  "antd": "5.24.2"
20
20
  };
@@ -225,9 +225,10 @@ function createMappingFetch(responseMapping) {
225
225
  };
226
226
  }
227
227
  function wrapWithStreamKeepAlive(model, options) {
228
- const originalStream = model._stream.bind(model);
228
+ const streamMethodName = typeof model._streamResponseChunks === "function" ? "_streamResponseChunks" : "_stream";
229
+ const originalStream = model[streamMethodName].bind(model);
229
230
  const { intervalMs, keepAliveContent } = options;
230
- model._stream = async function* (messages, opts, runManager) {
231
+ model[streamMethodName] = async function* (messages, opts, runManager) {
231
232
  const ChatGenerationChunk = getChatGenerationChunk();
232
233
  const AIMessageChunk = getAIMessageChunk();
233
234
  const baseIterator = originalStream(messages, opts, runManager);
@@ -235,9 +236,16 @@ function wrapWithStreamKeepAlive(model, options) {
235
236
  let streamDone = false;
236
237
  let streamError = null;
237
238
  let notifyReady = null;
239
+ let hasToolCallChunks = false;
240
+ let hasErrored = false;
238
241
  const consumer = (async () => {
242
+ var _a, _b;
239
243
  try {
240
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
+ }
241
249
  buffer.push(chunk);
242
250
  if (notifyReady) {
243
251
  notifyReady();
@@ -246,6 +254,11 @@ function wrapWithStreamKeepAlive(model, options) {
246
254
  }
247
255
  } catch (err) {
248
256
  streamError = err;
257
+ hasErrored = true;
258
+ if (notifyReady) {
259
+ notifyReady();
260
+ notifyReady = null;
261
+ }
249
262
  } finally {
250
263
  streamDone = true;
251
264
  if (notifyReady) {
@@ -259,6 +272,7 @@ function wrapWithStreamKeepAlive(model, options) {
259
272
  while (buffer.length > 0) {
260
273
  yield buffer.shift();
261
274
  }
275
+ hasToolCallChunks = false;
262
276
  if (streamDone) break;
263
277
  const waitForChunk = new Promise((resolve) => {
264
278
  notifyReady = resolve;
@@ -272,9 +286,14 @@ function wrapWithStreamKeepAlive(model, options) {
272
286
  ]);
273
287
  if (timer) clearTimeout(timer);
274
288
  if (result === "timeout" && !streamDone && buffer.length === 0) {
289
+ if (streamError || hasErrored) break;
290
+ if (hasToolCallChunks) continue;
275
291
  const keepAliveChunk = new ChatGenerationChunk({
276
- message: new AIMessageChunk({ content: KEEPALIVE_PREFIX + keepAliveContent }),
277
- text: KEEPALIVE_PREFIX + keepAliveContent
292
+ message: new AIMessageChunk({
293
+ content: KEEPALIVE_PREFIX,
294
+ additional_kwargs: { __keepalive: true }
295
+ }),
296
+ text: KEEPALIVE_PREFIX
278
297
  });
279
298
  yield keepAliveChunk;
280
299
  }
@@ -291,6 +310,81 @@ function wrapWithStreamKeepAlive(model, options) {
291
310
  function isKeepAlive(text) {
292
311
  return typeof text === "string" && text.startsWith(KEEPALIVE_PREFIX);
293
312
  }
313
+ function fixEmptyToolProperties(model) {
314
+ var _a;
315
+ const originalBind = (_a = model.bindTools) == null ? void 0 : _a.bind(model);
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
+ }
331
+ model.bindTools = function(tools, kwargs) {
332
+ var _a2;
333
+ const fixedTools = tools.map((tool) => {
334
+ var _a3, _b;
335
+ if (!tool || typeof tool !== "object") return tool;
336
+ if (typeof ((_a3 = tool.schema) == null ? void 0 : _a3.safeParse) === "function") {
337
+ return tool;
338
+ }
339
+ const schema = tool.schema;
340
+ if (schema && typeof schema === "object" && !schema.safeParse) {
341
+ const props = schema.properties;
342
+ if (props && typeof props === "object" && Object.keys(props).length === 0) {
343
+ return {
344
+ ...tool,
345
+ schema: {
346
+ ...schema,
347
+ properties: { ...PLACEHOLDER_PROP }
348
+ }
349
+ };
350
+ }
351
+ }
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) {
355
+ return {
356
+ ...tool,
357
+ function: {
358
+ ...tool.function,
359
+ parameters: {
360
+ ...funcParams,
361
+ properties: { ...PLACEHOLDER_PROP }
362
+ }
363
+ }
364
+ };
365
+ }
366
+ }
367
+ return tool;
368
+ });
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;
385
+ };
386
+ return model;
387
+ }
294
388
  class CustomLLMProvider extends import_plugin_ai.LLMProvider {
295
389
  get baseURL() {
296
390
  return null;
@@ -304,7 +398,9 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
304
398
  return safeParseJSON((_a = this.serviceOptions) == null ? void 0 : _a.responseConfig);
305
399
  }
306
400
  createModel() {
307
- const { baseURL, apiKey, disableStream, timeout, streamKeepAlive, keepAliveIntervalMs, keepAliveContent } = this.serviceOptions || {};
401
+ var _a;
402
+ const { apiKey, disableStream, timeout, streamKeepAlive, keepAliveIntervalMs, keepAliveContent } = this.serviceOptions || {};
403
+ const baseURL = (_a = this.serviceOptions) == null ? void 0 : _a.baseURL;
308
404
  const { responseFormat } = this.modelOptions || {};
309
405
  const reqConfig = this.requestConfig;
310
406
  const resConfig = this.responseConfig;
@@ -341,7 +437,8 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
341
437
  if (resConfig.responseMapping) {
342
438
  config.configuration.fetch = createMappingFetch(resConfig.responseMapping);
343
439
  }
344
- const model = new ChatOpenAI(config);
440
+ let model = new ChatOpenAI(config);
441
+ model = fixEmptyToolProperties(model);
345
442
  if (streamKeepAlive && !disableStream) {
346
443
  return wrapWithStreamKeepAlive(model, {
347
444
  intervalMs: Number(keepAliveIntervalMs) || 5e3,
@@ -354,11 +451,12 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
354
451
  const resConfig = this.responseConfig;
355
452
  const text = extractTextContent(chunk, resConfig.contentPath);
356
453
  if (isKeepAlive(text)) {
357
- return null;
454
+ return KEEPALIVE_PREFIX;
358
455
  }
359
456
  return stripToolCallTags(text);
360
457
  }
361
458
  parseResponseMessage(message) {
459
+ var _a, _b;
362
460
  const { content: rawContent, messageId, metadata, role, toolCalls, attachments, workContext } = message;
363
461
  const content = {
364
462
  ...rawContent ?? {},
@@ -379,6 +477,10 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
379
477
  content.content = content.content.replace(new RegExp(escapedPrefix + ".*?(?=" + escapedPrefix + "|$)", "g"), "");
380
478
  content.content = stripToolCallTags(content.content);
381
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
+ }
382
484
  return {
383
485
  key: messageId,
384
486
  content,
@@ -386,15 +488,43 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
386
488
  };
387
489
  }
388
490
  parseReasoningContent(chunk) {
389
- var _a;
491
+ var _a, _b, _c;
390
492
  const resConfig = this.responseConfig;
391
493
  const reasoningKey = resConfig.reasoningKey || "reasoning_content";
392
- 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]);
393
495
  if (reasoning && typeof reasoning === "string") {
394
496
  return { status: "streaming", content: reasoning };
395
497
  }
396
498
  return null;
397
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
+ }
398
528
  parseResponseError(err) {
399
529
  return (err == null ? void 0 : err.message) ?? "Unexpected LLM service error";
400
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.1.0",
6
+ "version": "1.2.0",
7
7
  "main": "dist/server/index.js",
8
8
  "nocobase": {
9
9
  "supportedVersions": [