@lobehub/lobehub 2.0.0-next.197 → 2.0.0-next.198

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.198](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.197...v2.0.0-next.198)
6
+
7
+ <sup>Released on **2026-01-03**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Support thoughtSignature for openrouter.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Support thoughtSignature for openrouter, closes [#11117](https://github.com/lobehub/lobe-chat/issues/11117) ([bf5d41e](https://github.com/lobehub/lobe-chat/commit/bf5d41e))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.197](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.196...v2.0.0-next.197)
6
31
 
7
32
  <sup>Released on **2026-01-03**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Support thoughtSignature for openrouter."
6
+ ]
7
+ },
8
+ "date": "2026-01-03",
9
+ "version": "2.0.0-next.198"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.197",
3
+ "version": "2.0.0-next.198",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1048,6 +1048,161 @@ describe('OpenAIStream', () => {
1048
1048
  ].map((i) => `${i}\n`),
1049
1049
  );
1050
1050
  });
1051
+
1052
+ it('should handle OpenRouter tool calls with thoughtSignature (for Gemini models)', async () => {
1053
+ // OpenRouter returns thoughtSignature in tool_calls for Gemini models
1054
+ // This is required for preserving reasoning blocks across turns
1055
+ // Ref: https://openrouter.ai/docs/guides/best-practices/reasoning-tokens
1056
+ const mockOpenAIStream = new ReadableStream({
1057
+ start(controller) {
1058
+ controller.enqueue({
1059
+ choices: [
1060
+ {
1061
+ delta: {
1062
+ tool_calls: [
1063
+ {
1064
+ function: { name: 'github__get_me', arguments: '{}' },
1065
+ id: 'call_123',
1066
+ index: 0,
1067
+ type: 'function',
1068
+ // OpenRouter adds thoughtSignature for Gemini 3 models
1069
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1070
+ },
1071
+ ],
1072
+ },
1073
+ index: 0,
1074
+ },
1075
+ ],
1076
+ id: 'or-123',
1077
+ });
1078
+
1079
+ controller.close();
1080
+ },
1081
+ });
1082
+
1083
+ const onToolCallMock = vi.fn();
1084
+
1085
+ const protocolStream = OpenAIStream(mockOpenAIStream, {
1086
+ callbacks: {
1087
+ onToolsCalling: onToolCallMock,
1088
+ },
1089
+ });
1090
+
1091
+ const decoder = new TextDecoder();
1092
+ const chunks = [];
1093
+
1094
+ // @ts-ignore
1095
+ for await (const chunk of protocolStream) {
1096
+ chunks.push(decoder.decode(chunk, { stream: true }));
1097
+ }
1098
+
1099
+ expect(chunks).toEqual([
1100
+ 'id: or-123\n',
1101
+ 'event: tool_calls\n',
1102
+ // thoughtSignature should be preserved in the output
1103
+ `data: [{"function":{"arguments":"{}","name":"github__get_me"},"id":"call_123","index":0,"type":"function","thoughtSignature":"ErEDCq4DAdHtim..."}]\n\n`,
1104
+ ]);
1105
+
1106
+ // Verify the callback receives thoughtSignature
1107
+ expect(onToolCallMock).toHaveBeenCalledWith({
1108
+ chunk: [
1109
+ {
1110
+ function: { arguments: '{}', name: 'github__get_me' },
1111
+ id: 'call_123',
1112
+ index: 0,
1113
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1114
+ type: 'function',
1115
+ },
1116
+ ],
1117
+ toolsCalling: [
1118
+ {
1119
+ function: { arguments: '{}', name: 'github__get_me' },
1120
+ id: 'call_123',
1121
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1122
+ type: 'function',
1123
+ },
1124
+ ],
1125
+ });
1126
+ });
1127
+
1128
+ it('should NOT include thoughtSignature in output when not present in tool call', async () => {
1129
+ // Standard tool calls without thoughtSignature should not include the field
1130
+ const mockOpenAIStream = new ReadableStream({
1131
+ start(controller) {
1132
+ controller.enqueue({
1133
+ choices: [
1134
+ {
1135
+ delta: {
1136
+ tool_calls: [
1137
+ {
1138
+ function: { name: 'search', arguments: '{"query":"test"}' },
1139
+ id: 'call_456',
1140
+ index: 0,
1141
+ type: 'function',
1142
+ // No thoughtSignature field
1143
+ },
1144
+ ],
1145
+ },
1146
+ index: 0,
1147
+ },
1148
+ ],
1149
+ id: 'standard-123',
1150
+ });
1151
+
1152
+ controller.close();
1153
+ },
1154
+ });
1155
+
1156
+ const onToolCallMock = vi.fn();
1157
+
1158
+ const protocolStream = OpenAIStream(mockOpenAIStream, {
1159
+ callbacks: {
1160
+ onToolsCalling: onToolCallMock,
1161
+ },
1162
+ });
1163
+
1164
+ const decoder = new TextDecoder();
1165
+ const chunks = [];
1166
+
1167
+ // @ts-ignore
1168
+ for await (const chunk of protocolStream) {
1169
+ chunks.push(decoder.decode(chunk, { stream: true }));
1170
+ }
1171
+
1172
+ expect(chunks).toEqual([
1173
+ 'id: standard-123\n',
1174
+ 'event: tool_calls\n',
1175
+ // thoughtSignature should NOT be in the output
1176
+ `data: [{"function":{"arguments":"{\\"query\\":\\"test\\"}","name":"search"},"id":"call_456","index":0,"type":"function"}]\n\n`,
1177
+ ]);
1178
+
1179
+ // Verify the callback does NOT receive thoughtSignature
1180
+ expect(onToolCallMock).toHaveBeenCalledWith({
1181
+ chunk: [
1182
+ {
1183
+ function: { arguments: '{"query":"test"}', name: 'search' },
1184
+ id: 'call_456',
1185
+ index: 0,
1186
+ // thoughtSignature should not be present
1187
+ type: 'function',
1188
+ },
1189
+ ],
1190
+ toolsCalling: [
1191
+ {
1192
+ function: { arguments: '{"query":"test"}', name: 'search' },
1193
+ id: 'call_456',
1194
+ // thoughtSignature should not be present
1195
+ type: 'function',
1196
+ },
1197
+ ],
1198
+ });
1199
+
1200
+ // Verify thoughtSignature is not in the chunk
1201
+ expect(onToolCallMock.mock.calls[0][0].chunk[0]).not.toHaveProperty('thoughtSignature');
1202
+ expect(onToolCallMock.mock.calls[0][0].toolsCalling[0]).not.toHaveProperty(
1203
+ 'thoughtSignature',
1204
+ );
1205
+ });
1051
1206
  });
1052
1207
 
1053
1208
  describe('Reasoning', () => {
@@ -20,6 +20,23 @@ import {
20
20
  generateToolCallId,
21
21
  } from '../protocol';
22
22
 
23
+ /**
24
+ * Extended type for OpenAI tool calls that includes provider-specific extensions
25
+ * like OpenRouter's thoughtSignature for Gemini models
26
+ */
27
+ type OpenAIExtendedToolCall = OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall & {
28
+ thoughtSignature?: string;
29
+ };
30
+
31
+ /**
32
+ * Type guard to check if a tool call has thoughtSignature
33
+ */
34
+ const hasThoughtSignature = (
35
+ toolCall: OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall,
36
+ ): toolCall is OpenAIExtendedToolCall => {
37
+ return 'thoughtSignature' in toolCall && typeof toolCall.thoughtSignature === 'string';
38
+ };
39
+
23
40
  // Process markdown base64 images: extract URLs and clean text in one pass
24
41
  const processMarkdownBase64Images = (text: string): { cleanedText: string; urls: string[] } => {
25
42
  if (!text) return { cleanedText: text, urls: [] };
@@ -150,7 +167,7 @@ const transformOpenAIStream = (
150
167
  };
151
168
  }
152
169
 
153
- return {
170
+ const baseData: StreamToolCallChunkData = {
154
171
  function: {
155
172
  arguments: value.function?.arguments ?? '',
156
173
  name: value.function?.name ?? null,
@@ -170,6 +187,14 @@ const transformOpenAIStream = (
170
187
  index: typeof value.index !== 'undefined' ? value.index : index,
171
188
  type: value.type || 'function',
172
189
  };
190
+
191
+ // OpenRouter returns thoughtSignature in tool_calls for Gemini models (e.g. gemini-3-flash-preview)
192
+ // [{"id":"call_123","type":"function","function":{"name":"get_weather","arguments":"{}"},"thoughtSignature":"abc123"}]
193
+ if (hasThoughtSignature(value)) {
194
+ baseData.thoughtSignature = value.thoughtSignature;
195
+ }
196
+
197
+ return baseData;
173
198
  }),
174
199
  id: chunk.id,
175
200
  type: 'tool_calls',