plugin-custom-llm 1.3.1 → 1.3.2
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.
|
@@ -542,44 +542,51 @@ function fixEmptyToolProperties(model) {
|
|
|
542
542
|
};
|
|
543
543
|
return model;
|
|
544
544
|
}
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
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
|
-
}
|
|
545
|
+
function sanitizeGenerateResult(result) {
|
|
546
|
+
if (!result) return result;
|
|
547
|
+
for (const gen of (result == null ? void 0 : result.generations) ?? []) {
|
|
548
|
+
const msg = gen == null ? void 0 : gen.message;
|
|
549
|
+
if (msg == null ? void 0 : msg.tool_calls) {
|
|
550
|
+
for (const tc of msg.tool_calls) {
|
|
551
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
558
552
|
}
|
|
559
|
-
|
|
560
|
-
};
|
|
553
|
+
}
|
|
561
554
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
function sanitizeStreamChunk(chunk) {
|
|
558
|
+
const msg = chunk == null ? void 0 : chunk.message;
|
|
559
|
+
if (msg == null ? void 0 : msg.tool_call_chunks) {
|
|
560
|
+
for (const tc of msg.tool_call_chunks) {
|
|
561
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (msg == null ? void 0 : msg.tool_calls) {
|
|
565
|
+
for (const tc of msg.tool_calls) {
|
|
566
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return chunk;
|
|
570
|
+
}
|
|
571
|
+
function createSanitizedChatClass(BaseClass) {
|
|
572
|
+
return class SanitizedChatModel extends BaseClass {
|
|
573
|
+
async _generate(messages, options, runManager) {
|
|
574
|
+
const result = await super._generate(messages, options, runManager);
|
|
575
|
+
return sanitizeGenerateResult(result);
|
|
576
|
+
}
|
|
577
|
+
async *_streamResponseChunks(messages, options, runManager) {
|
|
578
|
+
for await (const chunk of super._streamResponseChunks(messages, options, runManager)) {
|
|
579
|
+
yield sanitizeStreamChunk(chunk);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async *_stream(messages, options, runManager) {
|
|
583
|
+
if (typeof super._stream === "function") {
|
|
584
|
+
for await (const chunk of super._stream(messages, options, runManager)) {
|
|
585
|
+
yield sanitizeStreamChunk(chunk);
|
|
577
586
|
}
|
|
578
|
-
yield chunk;
|
|
579
587
|
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return model;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
583
590
|
}
|
|
584
591
|
class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
585
592
|
get baseURL() {
|
|
@@ -618,7 +625,8 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
|
618
625
|
if (reqConfig.extraBody && typeof reqConfig.extraBody === "object") {
|
|
619
626
|
Object.assign(modelKwargs, reqConfig.extraBody);
|
|
620
627
|
}
|
|
621
|
-
const
|
|
628
|
+
const BaseChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
|
|
629
|
+
const ChatClass = createSanitizedChatClass(BaseChatClass);
|
|
622
630
|
const config = {
|
|
623
631
|
apiKey,
|
|
624
632
|
...this.modelOptions,
|
|
@@ -644,7 +652,6 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
|
|
|
644
652
|
}
|
|
645
653
|
let model = new ChatClass(config);
|
|
646
654
|
model = fixEmptyToolProperties(model);
|
|
647
|
-
model = wrapWithToolCallIdSanitizer(model);
|
|
648
655
|
if (streamKeepAlive && !disableStream) {
|
|
649
656
|
return wrapWithStreamKeepAlive(model, {
|
|
650
657
|
intervalMs: Number(keepAliveIntervalMs) || 5e3,
|
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.3.
|
|
6
|
+
"version": "1.3.2",
|
|
7
7
|
"main": "dist/server/index.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
@@ -727,53 +727,66 @@ function fixEmptyToolProperties(model: any) {
|
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
/**
|
|
730
|
-
*
|
|
731
|
-
* Gemini models can return IDs like `call_xxx__thought__<long_base64>`
|
|
732
|
-
* which are too long for langgraph to handle on message replay.
|
|
733
|
-
* This strips the `__thought__...` suffix at the model output level
|
|
734
|
-
* so downstream code (convertAIMessage, etc.) only sees clean IDs.
|
|
730
|
+
* Sanitize all tool call IDs in a ChatResult (used after _generate).
|
|
735
731
|
*/
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
const msg = gen?.message;
|
|
744
|
-
if (msg?.tool_calls) {
|
|
745
|
-
for (const tc of msg.tool_calls) {
|
|
746
|
-
tc.id = sanitizeToolCallId(tc.id);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
732
|
+
function sanitizeGenerateResult(result: any): any {
|
|
733
|
+
if (!result) return result;
|
|
734
|
+
for (const gen of result?.generations ?? []) {
|
|
735
|
+
const msg = gen?.message;
|
|
736
|
+
if (msg?.tool_calls) {
|
|
737
|
+
for (const tc of msg.tool_calls) {
|
|
738
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
749
739
|
}
|
|
750
|
-
|
|
751
|
-
};
|
|
740
|
+
}
|
|
752
741
|
}
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
753
744
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
tc.id = sanitizeToolCallId(tc.id);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
yield chunk;
|
|
772
|
-
}
|
|
773
|
-
};
|
|
745
|
+
/**
|
|
746
|
+
* Sanitize tool call IDs in a streaming chunk.
|
|
747
|
+
*/
|
|
748
|
+
function sanitizeStreamChunk(chunk: any): any {
|
|
749
|
+
const msg = chunk?.message;
|
|
750
|
+
if (msg?.tool_call_chunks) {
|
|
751
|
+
for (const tc of msg.tool_call_chunks) {
|
|
752
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (msg?.tool_calls) {
|
|
756
|
+
for (const tc of msg.tool_calls) {
|
|
757
|
+
tc.id = sanitizeToolCallId(tc.id);
|
|
758
|
+
}
|
|
774
759
|
}
|
|
760
|
+
return chunk;
|
|
761
|
+
}
|
|
775
762
|
|
|
776
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Create a subclass of the given ChatModel class that sanitizes tool call IDs
|
|
765
|
+
* in all outputs. Gemini models return IDs like `call_xxx__thought__<long_base64>`
|
|
766
|
+
* which are too long for langgraph. Using class-level overrides (instead of
|
|
767
|
+
* instance patching) ensures the sanitization survives bindTools/RunnableBinding.
|
|
768
|
+
*/
|
|
769
|
+
function createSanitizedChatClass(BaseClass: any) {
|
|
770
|
+
return class SanitizedChatModel extends BaseClass {
|
|
771
|
+
async _generate(messages: any[], options: any, runManager?: any) {
|
|
772
|
+
const result = await super._generate(messages, options, runManager);
|
|
773
|
+
return sanitizeGenerateResult(result);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async *_streamResponseChunks(messages: any[], options: any, runManager?: any) {
|
|
777
|
+
for await (const chunk of super._streamResponseChunks(messages, options, runManager)) {
|
|
778
|
+
yield sanitizeStreamChunk(chunk);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async *_stream(messages: any[], options: any, runManager?: any) {
|
|
783
|
+
if (typeof super._stream === 'function') {
|
|
784
|
+
for await (const chunk of super._stream(messages, options, runManager)) {
|
|
785
|
+
yield sanitizeStreamChunk(chunk);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
777
790
|
}
|
|
778
791
|
|
|
779
792
|
export class CustomLLMProvider extends LLMProvider {
|
|
@@ -816,7 +829,11 @@ export class CustomLLMProvider extends LLMProvider {
|
|
|
816
829
|
// Issue #4: Use ReasoningChatOpenAI when enableReasoning is set.
|
|
817
830
|
// This ensures reasoning_content is preserved and patched back into
|
|
818
831
|
// assistant messages during tool call round-trips (required by DeepSeek-R1, etc.)
|
|
819
|
-
|
|
832
|
+
// Wrap with tool call ID sanitizer at the class level — ensures
|
|
833
|
+
// __thought__<base64> suffixes from Gemini are stripped in all code paths
|
|
834
|
+
// (invoke, stream, bindTools bindings) via prototype chain.
|
|
835
|
+
const BaseChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
|
|
836
|
+
const ChatClass = createSanitizedChatClass(BaseChatClass);
|
|
820
837
|
const config: Record<string, any> = {
|
|
821
838
|
apiKey,
|
|
822
839
|
...this.modelOptions,
|
|
@@ -855,9 +872,6 @@ export class CustomLLMProvider extends LLMProvider {
|
|
|
855
872
|
// Fix empty tool properties for strict providers (Gemini, etc.)
|
|
856
873
|
model = fixEmptyToolProperties(model);
|
|
857
874
|
|
|
858
|
-
// Sanitize Gemini's __thought__<base64> suffixes in tool call IDs
|
|
859
|
-
model = wrapWithToolCallIdSanitizer(model);
|
|
860
|
-
|
|
861
875
|
// Wrap with keepalive proxy if enabled (and streaming is not disabled)
|
|
862
876
|
if (streamKeepAlive && !disableStream) {
|
|
863
877
|
return wrapWithStreamKeepAlive(model, {
|