@lobehub/chat 1.53.0 → 1.53.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.53.1](https://github.com/lobehub/lobe-chat/compare/v1.53.0...v1.53.1)
6
+
7
+ <sup>Released on **2025-02-12**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix reasoning output for OpenRouter reasoning models like deepseek-r1.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix reasoning output for OpenRouter reasoning models like deepseek-r1, closes [#5903](https://github.com/lobehub/lobe-chat/issues/5903) [#5766](https://github.com/lobehub/lobe-chat/issues/5766) ([bfd9317](https://github.com/lobehub/lobe-chat/commit/bfd9317))
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 1.53.0](https://github.com/lobehub/lobe-chat/compare/v1.52.19...v1.53.0)
6
31
 
7
32
  <sup>Released on **2025-02-11**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix reasoning output for OpenRouter reasoning models like deepseek-r1."
6
+ ]
7
+ },
8
+ "date": "2025-02-12",
9
+ "version": "1.53.1"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.53.0",
3
+ "version": "1.53.1",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -214,6 +214,19 @@ const OpenRouter: ModelProviderCard = {
214
214
  },
215
215
  releasedAt: '2024-09-05',
216
216
  },
217
+ {
218
+ contextWindowTokens: 163_840,
219
+ description: 'DeepSeek-R1',
220
+ displayName: 'DeepSeek R1',
221
+ enabled: true,
222
+ functionCall: false,
223
+ id: 'deepseek/deepseek-r1',
224
+ pricing: {
225
+ input: 3,
226
+ output: 8,
227
+ },
228
+ releasedAt: '2025-01-20',
229
+ },
217
230
  {
218
231
  contextWindowTokens: 131_072,
219
232
  description:
@@ -79,14 +79,14 @@ describe('LobeOpenRouterAI', () => {
79
79
 
80
80
  // Assert
81
81
  expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
82
- {
82
+ expect.objectContaining({
83
83
  max_tokens: 1024,
84
84
  messages: [{ content: 'Hello', role: 'user' }],
85
85
  stream: true,
86
86
  model: 'mistralai/mistral-7b-instruct:free',
87
87
  temperature: 0.7,
88
88
  top_p: 1,
89
- },
89
+ }),
90
90
  { headers: { Accept: '*/*' } },
91
91
  );
92
92
  expect(result).toBeInstanceOf(Response);
@@ -6,6 +6,15 @@ import { OpenRouterModelCard } from './type';
6
6
 
7
7
  export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
8
8
  baseURL: 'https://openrouter.ai/api/v1',
9
+ chatCompletion: {
10
+ handlePayload: (payload) => {
11
+ return {
12
+ ...payload,
13
+ include_reasoning: true,
14
+ stream: payload.stream ?? true,
15
+ } as any;
16
+ },
17
+ },
9
18
  constructorOptions: {
10
19
  defaultHeaders: {
11
20
  'HTTP-Referer': 'https://chat-preview.lobehub.com',
@@ -17,10 +26,7 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
17
26
  },
18
27
  models: {
19
28
  transformModel: (m) => {
20
- const visionKeywords = [
21
- 'qwen/qvq',
22
- 'vision',
23
- ];
29
+ const visionKeywords = ['qwen/qvq', 'vision'];
24
30
 
25
31
  const reasoningKeywords = [
26
32
  'deepseek/deepseek-r1',
@@ -28,7 +34,7 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
28
34
  'openai/o3',
29
35
  'qwen/qvq',
30
36
  'qwen/qwq',
31
- 'thinking'
37
+ 'thinking',
32
38
  ];
33
39
 
34
40
  const model = m as unknown as OpenRouterModelCard;
@@ -45,11 +51,11 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
45
51
  typeof model.top_provider.max_completion_tokens === 'number'
46
52
  ? model.top_provider.max_completion_tokens
47
53
  : undefined,
48
- reasoning: reasoningKeywords.some(keyword => model.id.toLowerCase().includes(keyword)),
54
+ reasoning: reasoningKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)),
49
55
  vision:
50
56
  model.description.includes('vision') ||
51
57
  model.description.includes('multimodal') ||
52
- visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword)),
58
+ visionKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)),
53
59
  };
54
60
  },
55
61
  },
@@ -1375,5 +1375,206 @@ describe('OpenAIStream', () => {
1375
1375
  ].map((i) => `${i}\n`),
1376
1376
  );
1377
1377
  });
1378
+
1379
+ it('should handle reasoning key from OpenRouter response', async () => {
1380
+ const data = [
1381
+ {
1382
+ id: '1',
1383
+ object: 'chat.completion.chunk',
1384
+ created: 1737563070,
1385
+ model: 'deepseek-reasoner',
1386
+ system_fingerprint: 'fp_1c5d8833bc',
1387
+ choices: [
1388
+ {
1389
+ index: 0,
1390
+ delta: { role: 'assistant', reasoning: '' },
1391
+ logprobs: null,
1392
+ finish_reason: null,
1393
+ },
1394
+ ],
1395
+ },
1396
+ {
1397
+ id: '1',
1398
+ object: 'chat.completion.chunk',
1399
+ created: 1737563070,
1400
+ model: 'deepseek-reasoner',
1401
+ system_fingerprint: 'fp_1c5d8833bc',
1402
+ choices: [
1403
+ {
1404
+ index: 0,
1405
+ delta: { reasoning: '您好' },
1406
+ logprobs: null,
1407
+ finish_reason: null,
1408
+ },
1409
+ ],
1410
+ },
1411
+ {
1412
+ id: '1',
1413
+ object: 'chat.completion.chunk',
1414
+ created: 1737563070,
1415
+ model: 'deepseek-reasoner',
1416
+ system_fingerprint: 'fp_1c5d8833bc',
1417
+ choices: [
1418
+ {
1419
+ index: 0,
1420
+ delta: { reasoning: '!' },
1421
+ logprobs: null,
1422
+ finish_reason: null,
1423
+ },
1424
+ ],
1425
+ },
1426
+ {
1427
+ id: '1',
1428
+ object: 'chat.completion.chunk',
1429
+ created: 1737563070,
1430
+ model: 'deepseek-reasoner',
1431
+ system_fingerprint: 'fp_1c5d8833bc',
1432
+ choices: [
1433
+ {
1434
+ index: 0,
1435
+ delta: { content: '你好', reasoning: null },
1436
+ logprobs: null,
1437
+ finish_reason: null,
1438
+ },
1439
+ ],
1440
+ },
1441
+ {
1442
+ id: '1',
1443
+ object: 'chat.completion.chunk',
1444
+ created: 1737563070,
1445
+ model: 'deepseek-reasoner',
1446
+ system_fingerprint: 'fp_1c5d8833bc',
1447
+ choices: [
1448
+ {
1449
+ index: 0,
1450
+ delta: { content: '很高兴', reasoning: null },
1451
+ logprobs: null,
1452
+ finish_reason: null,
1453
+ },
1454
+ ],
1455
+ },
1456
+ {
1457
+ id: '1',
1458
+ object: 'chat.completion.chunk',
1459
+ created: 1737563070,
1460
+ model: 'deepseek-reasoner',
1461
+ system_fingerprint: 'fp_1c5d8833bc',
1462
+ choices: [
1463
+ {
1464
+ index: 0,
1465
+ delta: { content: '为您', reasoning: null },
1466
+ logprobs: null,
1467
+ finish_reason: null,
1468
+ },
1469
+ ],
1470
+ },
1471
+ {
1472
+ id: '1',
1473
+ object: 'chat.completion.chunk',
1474
+ created: 1737563070,
1475
+ model: 'deepseek-reasoner',
1476
+ system_fingerprint: 'fp_1c5d8833bc',
1477
+ choices: [
1478
+ {
1479
+ index: 0,
1480
+ delta: { content: '提供', reasoning: null },
1481
+ logprobs: null,
1482
+ finish_reason: null,
1483
+ },
1484
+ ],
1485
+ },
1486
+ {
1487
+ id: '1',
1488
+ object: 'chat.completion.chunk',
1489
+ created: 1737563070,
1490
+ model: 'deepseek-reasoner',
1491
+ system_fingerprint: 'fp_1c5d8833bc',
1492
+ choices: [
1493
+ {
1494
+ index: 0,
1495
+ delta: { content: '帮助。', reasoning: null },
1496
+ logprobs: null,
1497
+ finish_reason: null,
1498
+ },
1499
+ ],
1500
+ },
1501
+ {
1502
+ id: '1',
1503
+ object: 'chat.completion.chunk',
1504
+ created: 1737563070,
1505
+ model: 'deepseek-reasoner',
1506
+ system_fingerprint: 'fp_1c5d8833bc',
1507
+ choices: [
1508
+ {
1509
+ index: 0,
1510
+ delta: { content: '', reasoning: null },
1511
+ logprobs: null,
1512
+ finish_reason: 'stop',
1513
+ },
1514
+ ],
1515
+ usage: {
1516
+ prompt_tokens: 6,
1517
+ completion_tokens: 104,
1518
+ total_tokens: 110,
1519
+ prompt_tokens_details: { cached_tokens: 0 },
1520
+ completion_tokens_details: { reasoning_tokens: 70 },
1521
+ prompt_cache_hit_tokens: 0,
1522
+ prompt_cache_miss_tokens: 6,
1523
+ },
1524
+ },
1525
+ ];
1526
+
1527
+ const mockOpenAIStream = new ReadableStream({
1528
+ start(controller) {
1529
+ data.forEach((chunk) => {
1530
+ controller.enqueue(chunk);
1531
+ });
1532
+
1533
+ controller.close();
1534
+ },
1535
+ });
1536
+
1537
+ const protocolStream = OpenAIStream(mockOpenAIStream);
1538
+
1539
+ const decoder = new TextDecoder();
1540
+ const chunks = [];
1541
+
1542
+ // @ts-ignore
1543
+ for await (const chunk of protocolStream) {
1544
+ chunks.push(decoder.decode(chunk, { stream: true }));
1545
+ }
1546
+
1547
+ expect(chunks).toEqual(
1548
+ [
1549
+ 'id: 1',
1550
+ 'event: reasoning',
1551
+ `data: ""\n`,
1552
+ 'id: 1',
1553
+ 'event: reasoning',
1554
+ `data: "您好"\n`,
1555
+ 'id: 1',
1556
+ 'event: reasoning',
1557
+ `data: "!"\n`,
1558
+ 'id: 1',
1559
+ 'event: text',
1560
+ `data: "你好"\n`,
1561
+ 'id: 1',
1562
+ 'event: text',
1563
+ `data: "很高兴"\n`,
1564
+ 'id: 1',
1565
+ 'event: text',
1566
+ `data: "为您"\n`,
1567
+ 'id: 1',
1568
+ 'event: text',
1569
+ `data: "提供"\n`,
1570
+ 'id: 1',
1571
+ 'event: text',
1572
+ `data: "帮助。"\n`,
1573
+ 'id: 1',
1574
+ 'event: stop',
1575
+ `data: "stop"\n`,
1576
+ ].map((i) => `${i}\n`),
1577
+ );
1578
+ });
1378
1579
  });
1379
1580
  });
@@ -88,8 +88,12 @@ export const transformOpenAIStream = (
88
88
  }
89
89
 
90
90
  if (item.delta) {
91
- let reasoning_content =
92
- 'reasoning_content' in item.delta ? item.delta.reasoning_content : null;
91
+ let reasoning_content = (() => {
92
+ if ('reasoning_content' in item.delta) return item.delta.reasoning_content;
93
+ if ('reasoning' in item.delta) return item.delta.reasoning;
94
+ return null;
95
+ })();
96
+
93
97
  let content = 'content' in item.delta ? item.delta.content : null;
94
98
 
95
99
  // DeepSeek reasoner will put thinking in the reasoning_content field