plugin-custom-llm 1.3.0 → 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.
@@ -66,6 +66,11 @@ function getChatOpenAICompletions() {
66
66
  }
67
67
  return _ChatOpenAICompletions;
68
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
+ }
69
74
  function getToolCallsKey(toolCalls = []) {
70
75
  return toolCalls.map((tc) => {
71
76
  var _a;
@@ -537,6 +542,45 @@ function fixEmptyToolProperties(model) {
537
542
  };
538
543
  return model;
539
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
+ }
540
584
  class CustomLLMProvider extends import_plugin_ai.LLMProvider {
541
585
  get baseURL() {
542
586
  return null;
@@ -600,6 +644,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
600
644
  }
601
645
  let model = new ChatClass(config);
602
646
  model = fixEmptyToolProperties(model);
647
+ model = wrapWithToolCallIdSanitizer(model);
603
648
  if (streamKeepAlive && !disableStream) {
604
649
  return wrapWithStreamKeepAlive(model, {
605
650
  intervalMs: Number(keepAliveIntervalMs) || 5e3,
@@ -627,7 +672,7 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
627
672
  workContext
628
673
  };
629
674
  if (toolCalls) {
630
- content.tool_calls = toolCalls;
675
+ content.tool_calls = Array.isArray(toolCalls) ? toolCalls.map((tc) => ({ ...tc, id: sanitizeToolCallId(tc.id) })) : toolCalls;
631
676
  }
632
677
  if (Array.isArray(content.content)) {
633
678
  const textBlocks = content.content.filter((block) => block.type === "text");
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.0",
6
+ "version": "1.3.1",
7
7
  "main": "dist/server/index.js",
8
8
  "files": [
9
9
  "dist",
@@ -50,6 +50,17 @@ function getChatOpenAICompletions() {
50
50
  return _ChatOpenAICompletions;
51
51
  }
52
52
 
53
+ /**
54
+ * Sanitize a tool call ID by stripping the `__thought__<base64>` suffix
55
+ * that Gemini models append during streaming. The suffix is excessively
56
+ * long and causes errors when langgraph reads messages back from history.
57
+ */
58
+ function sanitizeToolCallId(id: string | undefined): string | undefined {
59
+ if (!id || typeof id !== 'string') return id;
60
+ const idx = id.indexOf('__thought__');
61
+ return idx !== -1 ? id.substring(0, idx) : id;
62
+ }
63
+
53
64
  /**
54
65
  * Build tool_calls key for reasoning content map lookup.
55
66
  */
@@ -715,6 +726,56 @@ function fixEmptyToolProperties(model: any) {
715
726
  return model;
716
727
  }
717
728
 
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.
735
+ */
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
+ }
749
+ }
750
+ return result;
751
+ };
752
+ }
753
+
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
+ };
774
+ }
775
+
776
+ return model;
777
+ }
778
+
718
779
  export class CustomLLMProvider extends LLMProvider {
719
780
  get baseURL() {
720
781
  return null;
@@ -794,6 +855,9 @@ export class CustomLLMProvider extends LLMProvider {
794
855
  // Fix empty tool properties for strict providers (Gemini, etc.)
795
856
  model = fixEmptyToolProperties(model);
796
857
 
858
+ // Sanitize Gemini's __thought__<base64> suffixes in tool call IDs
859
+ model = wrapWithToolCallIdSanitizer(model);
860
+
797
861
  // Wrap with keepalive proxy if enabled (and streaming is not disabled)
798
862
  if (streamKeepAlive && !disableStream) {
799
863
  return wrapWithStreamKeepAlive(model, {
@@ -830,7 +894,9 @@ export class CustomLLMProvider extends LLMProvider {
830
894
  };
831
895
 
832
896
  if (toolCalls) {
833
- content.tool_calls = toolCalls;
897
+ content.tool_calls = Array.isArray(toolCalls)
898
+ ? toolCalls.map((tc: any) => ({ ...tc, id: sanitizeToolCallId(tc.id) }))
899
+ : toolCalls;
834
900
  }
835
901
 
836
902
  if (Array.isArray(content.content)) {