@lobehub/lobehub 2.0.0-next.197 → 2.0.0-next.199
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 +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +28 -8
- package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +9 -3
- package/packages/model-runtime/src/core/streams/openai/openai.test.ts +155 -0
- package/packages/model-runtime/src/core/streams/openai/openai.ts +26 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.199](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.198...v2.0.0-next.199)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-03**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Filter empty assistant messages for Anthropic API.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Filter empty assistant messages for Anthropic API, closes [#11129](https://github.com/lobehub/lobe-chat/issues/11129) ([7af750b](https://github.com/lobehub/lobe-chat/commit/7af750b))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.198](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.197...v2.0.0-next.198)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-03**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Support thoughtSignature for openrouter.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Support thoughtSignature for openrouter, closes [#11117](https://github.com/lobehub/lobe-chat/issues/11117) ([bf5d41e](https://github.com/lobehub/lobe-chat/commit/bf5d41e))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.197](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.196...v2.0.0-next.197)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-03**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Filter empty assistant messages for Anthropic API."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2026-01-03",
|
|
9
|
+
"version": "2.0.0-next.199"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"fixes": [
|
|
14
|
+
"Support thoughtSignature for openrouter."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2026-01-03",
|
|
18
|
+
"version": "2.0.0-next.198"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"improvements": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.199",
|
|
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",
|
|
@@ -155,9 +155,9 @@ describe('anthropicHelpers', () => {
|
|
|
155
155
|
role: 'user',
|
|
156
156
|
};
|
|
157
157
|
const result = await buildAnthropicMessage(message);
|
|
158
|
-
expect(result
|
|
159
|
-
expect(result
|
|
160
|
-
expect((result
|
|
158
|
+
expect(result!.role).toBe('user');
|
|
159
|
+
expect(result!.content).toHaveLength(2);
|
|
160
|
+
expect((result!.content[1] as any).type).toBe('image');
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
it('should correctly convert tool message', async () => {
|
|
@@ -167,8 +167,8 @@ describe('anthropicHelpers', () => {
|
|
|
167
167
|
tool_call_id: 'tool123',
|
|
168
168
|
};
|
|
169
169
|
const result = await buildAnthropicMessage(message);
|
|
170
|
-
expect(result
|
|
171
|
-
expect(result
|
|
170
|
+
expect(result!.role).toBe('user');
|
|
171
|
+
expect(result!.content).toEqual([
|
|
172
172
|
{
|
|
173
173
|
content: 'Tool result content',
|
|
174
174
|
tool_use_id: 'tool123',
|
|
@@ -193,8 +193,8 @@ describe('anthropicHelpers', () => {
|
|
|
193
193
|
],
|
|
194
194
|
};
|
|
195
195
|
const result = await buildAnthropicMessage(message);
|
|
196
|
-
expect(result
|
|
197
|
-
expect(result
|
|
196
|
+
expect(result!.role).toBe('assistant');
|
|
197
|
+
expect(result!.content).toEqual([
|
|
198
198
|
{ text: 'Here is the result:', type: 'text' },
|
|
199
199
|
{
|
|
200
200
|
id: 'call1',
|
|
@@ -266,15 +266,35 @@ describe('anthropicHelpers', () => {
|
|
|
266
266
|
|
|
267
267
|
const contents = await buildAnthropicMessages(messages);
|
|
268
268
|
|
|
269
|
+
// Empty assistant messages should be filtered out
|
|
269
270
|
expect(contents).toEqual([
|
|
270
271
|
{
|
|
271
272
|
content: '## Tools\n\nYou can use these tools',
|
|
272
273
|
role: 'user',
|
|
273
274
|
},
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should filter out assistant message with whitespace-only content', async () => {
|
|
279
|
+
const messages: OpenAIChatMessage[] = [
|
|
274
280
|
{
|
|
275
|
-
content: '',
|
|
281
|
+
content: 'Hello',
|
|
282
|
+
role: 'user',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
content: ' \n\t ',
|
|
276
286
|
role: 'assistant',
|
|
277
287
|
},
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const contents = await buildAnthropicMessages(messages);
|
|
291
|
+
|
|
292
|
+
// Whitespace-only assistant messages should be filtered out
|
|
293
|
+
expect(contents).toEqual([
|
|
294
|
+
{
|
|
295
|
+
content: 'Hello',
|
|
296
|
+
role: 'user',
|
|
297
|
+
},
|
|
278
298
|
]);
|
|
279
299
|
});
|
|
280
300
|
it('should correctly convert OpenAI tool message to Anthropic format', async () => {
|
|
@@ -62,7 +62,7 @@ const buildArrayContent = async (content: UserMessageContentPart[]) => {
|
|
|
62
62
|
|
|
63
63
|
export const buildAnthropicMessage = async (
|
|
64
64
|
message: OpenAIChatMessage,
|
|
65
|
-
): Promise<Anthropic.Messages.MessageParam> => {
|
|
65
|
+
): Promise<Anthropic.Messages.MessageParam | undefined> => {
|
|
66
66
|
const content = message.content as string | UserMessageContentPart[];
|
|
67
67
|
|
|
68
68
|
switch (message.role) {
|
|
@@ -118,7 +118,10 @@ export const buildAnthropicMessage = async (
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// or it's a plain assistant message
|
|
121
|
-
|
|
121
|
+
// Anthropic API requires non-empty content, filter out empty/whitespace-only content
|
|
122
|
+
const textContent = (content as string)?.trim();
|
|
123
|
+
if (!textContent) return undefined;
|
|
124
|
+
return { content: textContent, role: 'assistant' };
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
case 'function': {
|
|
@@ -176,7 +179,10 @@ export const buildAnthropicMessages = async (
|
|
|
176
179
|
}
|
|
177
180
|
} else {
|
|
178
181
|
const anthropicMessage = await buildAnthropicMessage(message);
|
|
179
|
-
messages.
|
|
182
|
+
// Filter out undefined messages (e.g., empty assistant messages)
|
|
183
|
+
if (anthropicMessage) {
|
|
184
|
+
messages.push(anthropicMessage);
|
|
185
|
+
}
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -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
|
-
|
|
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',
|