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 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
- }
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
- return result;
560
- };
553
+ }
561
554
  }
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
- }
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 ChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
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.1",
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
- * Wrap a chat model to sanitize tool call IDs in outputs.
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 wrapWithToolCallIdSanitizer(model: any) {
737
- // Patch _generate (used by invoke / non-streaming)
738
- const originalGenerate = model._generate?.bind(model);
739
- if (originalGenerate) {
740
- model._generate = async function (...args: any[]) {
741
- const result = await originalGenerate(...args);
742
- for (const gen of result?.generations ?? []) {
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
- return result;
751
- };
740
+ }
752
741
  }
742
+ return result;
743
+ }
753
744
 
754
- // Patch _streamResponseChunks or _stream (used by streamEvents / streaming)
755
- const streamMethod = typeof model._streamResponseChunks === 'function' ? '_streamResponseChunks' : '_stream';
756
- const originalStream = model[streamMethod]?.bind(model);
757
- if (originalStream) {
758
- model[streamMethod] = async function* (...args: any[]) {
759
- for await (const chunk of originalStream(...args)) {
760
- const msg = chunk?.message;
761
- if (msg?.tool_call_chunks) {
762
- for (const tc of msg.tool_call_chunks) {
763
- tc.id = sanitizeToolCallId(tc.id);
764
- }
765
- }
766
- if (msg?.tool_calls) {
767
- for (const tc of msg.tool_calls) {
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
- return model;
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
- const ChatClass = enableReasoning ? createReasoningChatClass() : getChatOpenAI();
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, {