@lobehub/chat 1.69.4 → 1.69.5

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,33 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.69.5](https://github.com/lobehub/lobe-chat/compare/v1.69.4...v1.69.5)
6
+
7
+ <sup>Released on **2025-03-09**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **chat**: Auto send message from URL.
12
+ - **misc**: Support openrouter claude 3.7 sonnet reasoning.
13
+
14
+ <br/>
15
+
16
+ <details>
17
+ <summary><kbd>Improvements and Fixes</kbd></summary>
18
+
19
+ #### Styles
20
+
21
+ - **chat**: Auto send message from URL, closes [#6497](https://github.com/lobehub/lobe-chat/issues/6497) ([30b2639](https://github.com/lobehub/lobe-chat/commit/30b2639))
22
+ - **misc**: Support openrouter claude 3.7 sonnet reasoning, closes [#6806](https://github.com/lobehub/lobe-chat/issues/6806) ([f1ffc2c](https://github.com/lobehub/lobe-chat/commit/f1ffc2c))
23
+
24
+ </details>
25
+
26
+ <div align="right">
27
+
28
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
29
+
30
+ </div>
31
+
5
32
  ### [Version 1.69.4](https://github.com/lobehub/lobe-chat/compare/v1.69.3...v1.69.4)
6
33
 
7
34
  <sup>Released on **2025-03-09**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Support openrouter claude 3.7 sonnet reasoning."
6
+ ]
7
+ },
8
+ "date": "2025-03-09",
9
+ "version": "1.69.5"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
@@ -170,6 +170,16 @@ show_message() {
170
170
  ;;
171
171
  esac
172
172
  ;;
173
+ tips_download_failed)
174
+ case $LANGUAGE in
175
+ zh_CN)
176
+ echo "$2 下载失败,请检查网络连接。"
177
+ ;;
178
+ *)
179
+ echo "$2 Download failed, please check the network connection."
180
+ ;;
181
+ esac
182
+ ;;
173
183
  tips_already_installed)
174
184
  case $LANGUAGE in
175
185
  zh_CN)
@@ -260,6 +270,30 @@ show_message() {
260
270
  ;;
261
271
  esac
262
272
  ;;
273
+ tips_no_docker_permission)
274
+ case $LANGUAGE in
275
+ zh_CN)
276
+ echo "WARN: 看起来当前用户没有 Docker 权限。"
277
+ echo "使用 'sudo usermod -aG docker $USER' 为用户分配 Docker 权限(可能需要重新启动 shell)。"
278
+ ;;
279
+ *)
280
+ echo "WARN: It look like the current user does not have Docker permissions."
281
+ echo "Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
282
+ ;;
283
+ esac
284
+ ;;
285
+ tips_init_database_failed)
286
+ case $LANGUAGE in
287
+ zh_CN)
288
+ echo "无法初始化数据库,为了避免你的数据重复初始化,请在首次成功启动时运行以下指令清空 Casdoor 初始配置文件:"
289
+ echo "echo '{}' > init_data.json"
290
+ ;;
291
+ *)
292
+ echo "Failed to initialize the database. To avoid your data being initialized repeatedly, run the following command to unmount the initial configuration file of Casdoor when you first start successfully:"
293
+ echo "echo '{}' > init_data.json"
294
+ ;;
295
+ esac
296
+ ;;
263
297
  ask_regenerate_secrets)
264
298
  case $LANGUAGE in
265
299
  zh_CN)
@@ -320,12 +354,27 @@ show_message() {
320
354
  ;;
321
355
  esac
322
356
  ;;
357
+ ask_init_database)
358
+ case $LANGUAGE in
359
+ zh_CN)
360
+ echo "是否初始化数据库?"
361
+ ;;
362
+ *)
363
+ echo "Do you want to initialize the database?"
364
+ ;;
365
+ esac
366
+ ;;
323
367
  esac
324
368
  }
325
369
 
326
370
  # Function to download files
327
371
  download_file() {
328
- wget -q --show-progress "$1" -O "$2"
372
+ wget --show-progress "$1" -O "$2"
373
+ # If run failed, exit
374
+ if [ $? -ne 0 ]; then
375
+ show_message "tips_download_failed" "$2"
376
+ exit 1
377
+ fi
329
378
  }
330
379
 
331
380
  print_centered() {
@@ -629,12 +678,54 @@ section_regenerate_secrets() {
629
678
  fi
630
679
  fi
631
680
  }
681
+
632
682
  show_message "ask_regenerate_secrets"
633
683
  ask "(y/n)" "y"
634
684
  if [[ "$ask_result" == "y" ]]; then
635
685
  section_regenerate_secrets
636
686
  fi
637
687
 
688
+ section_init_database() {
689
+ if ! command -v docker &> /dev/null ; then
690
+ echo "docker" $(show_message "tips_no_executable")
691
+ return 1
692
+ fi
693
+
694
+ if ! docker compose &> /dev/null ; then
695
+ echo "docker compose" $(show_message "tips_no_executable")
696
+ return 1
697
+ fi
698
+
699
+ # Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
700
+ # If this fails, the user probably does not have permissions for Docker.
701
+ # ref: https://github.com/paperless-ngx/paperless-ngx/blob/89e5c08a1fe4ca0b7641ae8fbd5554502199ae40/install-paperless-ngx.sh#L64-L72
702
+ if ! docker stats --no-stream &> /dev/null ; then
703
+ echo $(show_message "tips_no_docker_permission")
704
+ return 1
705
+ fi
706
+
707
+ docker compose pull
708
+ docker compose up --detach postgresql casdoor
709
+ # hopefully enough time for even the slower systems
710
+ sleep 15
711
+ docker compose stop
712
+
713
+ # Init finished, remove init mount
714
+ echo '{}' > init_data.json
715
+ }
716
+
717
+ show_message "ask_init_database"
718
+ ask "(y/n)" "y"
719
+ if [[ "$ask_result" == "y" ]]; then
720
+ # If return 1 means failed
721
+ section_init_database
722
+ if [ $? -ne 0 ]; then
723
+ echo $(show_message "tips_init_database_failed")
724
+ fi
725
+ else
726
+ show_message "tips_init_database_failed"
727
+ fi
728
+
638
729
  section_display_configurated_report() {
639
730
  # Display configuration reports
640
731
  echo $(show_message "security_secrect_regenerate_report")
@@ -27,6 +27,7 @@ Go to [Clerk](https://clerk.com?utm_source=lobehub\&utm_medium=docs) to register
27
27
  ```shell
28
28
  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
29
29
  CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
30
+ NEXT_PUBLIC_ENABLE_NEXT_AUTH=0
30
31
  ```
31
32
 
32
33
  ### Create and Configure Webhook in Clerk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.69.4",
3
+ "version": "1.69.5",
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",
@@ -180,7 +180,7 @@
180
180
  "langfuse": "3.29.1",
181
181
  "langfuse-core": "3.29.1",
182
182
  "lodash-es": "^4.17.21",
183
- "lucide-react": "^0.477.0",
183
+ "lucide-react": "^0.479.0",
184
184
  "mammoth": "^1.9.0",
185
185
  "mdast-util-to-markdown": "^2.1.2",
186
186
  "modern-screenshot": "^4.5.5",
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { browserless } from '../browserless';
4
+
5
+ describe('browserless', () => {
6
+ it('should throw BrowserlessInitError when env vars not set', async () => {
7
+ const originalEnv = { ...process.env };
8
+ process.env = { ...originalEnv };
9
+ delete process.env.BROWSERLESS_URL;
10
+ delete process.env.BROWSERLESS_TOKEN;
11
+
12
+ await expect(browserless('https://example.com', { filterOptions: {} })).rejects.toThrow(
13
+ '`BROWSERLESS_URL` or `BROWSERLESS_TOKEN` are required',
14
+ );
15
+
16
+ process.env = originalEnv;
17
+ });
18
+
19
+ it('should return undefined on fetch error', async () => {
20
+ process.env.BROWSERLESS_TOKEN = 'test-token';
21
+ global.fetch = vi.fn().mockRejectedValue(new Error('Fetch error'));
22
+
23
+ const result = await browserless('https://example.com', { filterOptions: {} });
24
+ expect(result).toBeUndefined();
25
+ });
26
+
27
+ it('should return undefined when content is empty', async () => {
28
+ process.env.BROWSERLESS_TOKEN = 'test-token';
29
+ global.fetch = vi.fn().mockResolvedValue({
30
+ text: vi.fn().mockResolvedValue('<html></html>'),
31
+ } as any);
32
+
33
+ const result = await browserless('https://example.com', { filterOptions: {} });
34
+ expect(result).toBeUndefined();
35
+ });
36
+
37
+ it('should return undefined when title is "Just a moment..."', async () => {
38
+ process.env.BROWSERLESS_TOKEN = 'test-token';
39
+ global.fetch = vi.fn().mockResolvedValue({
40
+ text: vi.fn().mockResolvedValue('<html><title>Just a moment...</title></html>'),
41
+ } as any);
42
+
43
+ const result = await browserless('https://example.com', { filterOptions: {} });
44
+ expect(result).toBeUndefined();
45
+ });
46
+
47
+ it('should return crawl result on successful fetch', async () => {
48
+ process.env.BROWSERLESS_TOKEN = 'test-token';
49
+ global.fetch = vi.fn().mockResolvedValue({
50
+ text: vi.fn().mockResolvedValue(`
51
+ <html>
52
+ <head>
53
+ <title>Test Title</title>
54
+ <meta name="description" content="Test Description">
55
+ </head>
56
+ <body>
57
+ <h1>Test Content</h1>
58
+ </body>
59
+ </html>
60
+ `),
61
+ } as any);
62
+
63
+ const result = await browserless('https://example.com', { filterOptions: {} });
64
+
65
+ expect(result).toEqual({
66
+ content: expect.any(String),
67
+ contentType: 'text',
68
+ description: expect.any(String),
69
+ length: expect.any(Number),
70
+ siteName: null,
71
+ title: 'Test Title',
72
+ url: 'https://example.com',
73
+ });
74
+ });
75
+
76
+ it('should use correct URL when BROWSERLESS_URL is provided', async () => {
77
+ const customUrl = 'https://custom.browserless.io';
78
+ const originalEnv = { ...process.env };
79
+ process.env.BROWSERLESS_TOKEN = 'test-token';
80
+ process.env.BROWSERLESS_URL = customUrl;
81
+ global.fetch = vi.fn().mockImplementation((url) => {
82
+ expect(url).toContain(customUrl);
83
+ return Promise.resolve({
84
+ text: () => Promise.resolve('<html><title>Test</title></html>'),
85
+ });
86
+ });
87
+
88
+ await browserless('https://example.com', { filterOptions: {} });
89
+
90
+ expect(global.fetch).toHaveBeenCalled();
91
+
92
+ process.env = originalEnv;
93
+ });
94
+ });
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { useSearchParams } from 'next/navigation';
4
+ import { useEffect } from 'react';
5
+
6
+ import { useSendMessage } from '@/features/ChatInput/useSend';
7
+ import { useChatStore } from '@/store/chat';
8
+
9
+ const MessageFromUrl = () => {
10
+ const updateInputMessage = useChatStore((s) => s.updateInputMessage);
11
+ const { send: sendMessage } = useSendMessage();
12
+ const searchParams = useSearchParams();
13
+
14
+ useEffect(() => {
15
+ const message = searchParams.get('message');
16
+ if (message) {
17
+ // Remove message from URL
18
+ const params = new URLSearchParams(searchParams.toString());
19
+ params.delete('message');
20
+ const newUrl = `${window.location.pathname}?${params.toString()}`;
21
+ window.history.replaceState({}, '', newUrl);
22
+
23
+ updateInputMessage(message);
24
+ sendMessage();
25
+ }
26
+ }, [searchParams, updateInputMessage, sendMessage]);
27
+
28
+ return null;
29
+ };
30
+
31
+ export default MessageFromUrl;
@@ -1,7 +1,7 @@
1
1
  import { Button, Space } from 'antd';
2
2
  import { createStyles } from 'antd-style';
3
3
  import { rgba } from 'polished';
4
- import { memo, useEffect, useState } from 'react';
4
+ import { Suspense, memo, useEffect, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
@@ -13,6 +13,7 @@ import { useChatStore } from '@/store/chat';
13
13
  import { chatSelectors } from '@/store/chat/selectors';
14
14
  import { isMacOS } from '@/utils/platform';
15
15
 
16
+ import MessageFromUrl from './MessageFromUrl';
16
17
  import SendMore from './SendMore';
17
18
  import ShortcutHint from './ShortcutHint';
18
19
 
@@ -67,49 +68,54 @@ const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
67
68
  }, [setIsMac]);
68
69
 
69
70
  return (
70
- <Flexbox
71
- align={'end'}
72
- className={styles.overrideAntdIcon}
73
- distribution={'space-between'}
74
- flex={'none'}
75
- gap={8}
76
- horizontal
77
- padding={'0 24px'}
78
- >
79
- <Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
80
- {expand && <LocalFiles />}
81
- </Flexbox>
82
- <Flexbox align={'center'} flex={'none'} gap={8} horizontal>
83
- <ShortcutHint />
84
- <SaveTopic />
85
- <Flexbox style={{ minWidth: 92 }}>
86
- {isAIGenerating ? (
87
- <Button
88
- className={styles.loadingButton}
89
- icon={<StopLoadingIcon />}
90
- onClick={stopGenerateMessage}
91
- >
92
- {t('input.stop')}
93
- </Button>
94
- ) : (
95
- <Space.Compact>
71
+ <>
72
+ <Suspense fallback={null}>
73
+ <MessageFromUrl />
74
+ </Suspense>
75
+ <Flexbox
76
+ align={'end'}
77
+ className={styles.overrideAntdIcon}
78
+ distribution={'space-between'}
79
+ flex={'none'}
80
+ gap={8}
81
+ horizontal
82
+ padding={'0 24px'}
83
+ >
84
+ <Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
85
+ {expand && <LocalFiles />}
86
+ </Flexbox>
87
+ <Flexbox align={'center'} flex={'none'} gap={8} horizontal>
88
+ <ShortcutHint />
89
+ <SaveTopic />
90
+ <Flexbox style={{ minWidth: 92 }}>
91
+ {isAIGenerating ? (
96
92
  <Button
97
- disabled={!canSend}
98
- loading={!canSend}
99
- onClick={() => {
100
- sendMessage();
101
- onExpandChange?.(false);
102
- }}
103
- type={'primary'}
93
+ className={styles.loadingButton}
94
+ icon={<StopLoadingIcon />}
95
+ onClick={stopGenerateMessage}
104
96
  >
105
- {t('input.send')}
97
+ {t('input.stop')}
106
98
  </Button>
107
- <SendMore disabled={!canSend} isMac={isMac} />
108
- </Space.Compact>
109
- )}
99
+ ) : (
100
+ <Space.Compact>
101
+ <Button
102
+ disabled={!canSend}
103
+ loading={!canSend}
104
+ onClick={() => {
105
+ sendMessage();
106
+ onExpandChange?.(false);
107
+ }}
108
+ type={'primary'}
109
+ >
110
+ {t('input.send')}
111
+ </Button>
112
+ <SendMore disabled={!canSend} isMac={isMac} />
113
+ </Space.Compact>
114
+ )}
115
+ </Flexbox>
110
116
  </Flexbox>
111
117
  </Flexbox>
112
- </Flexbox>
118
+ </>
113
119
  );
114
120
  });
115
121
 
@@ -137,6 +137,31 @@ const openrouterChatModels: AIChatModelCard[] = [
137
137
  releasedAt: '2024-06-20',
138
138
  type: 'chat',
139
139
  },
140
+ {
141
+ abilities: {
142
+ functionCall: true,
143
+ reasoning: true,
144
+ vision: true,
145
+ },
146
+ contextWindowTokens: 200_000,
147
+ description:
148
+ 'Claude 3.7 Sonnet 是 Anthropic 迄今为止最智能的模型,也是市场上首个混合推理模型。Claude 3.7 Sonnet 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。',
149
+ displayName: 'Claude 3.7 Sonnet',
150
+ enabled: true,
151
+ id: 'anthropic/claude-3.7-sonnet',
152
+ maxOutput: 8192,
153
+ pricing: {
154
+ cachedInput: 0.3,
155
+ input: 3,
156
+ output: 15,
157
+ writeCacheInput: 3.75,
158
+ },
159
+ releasedAt: '2025-02-24',
160
+ settings: {
161
+ extendParams: ['enableReasoning', 'reasoningBudgetToken'],
162
+ },
163
+ type: 'chat',
164
+ },
140
165
  {
141
166
  abilities: {
142
167
  functionCall: true,
@@ -258,7 +283,7 @@ const openrouterChatModels: AIChatModelCard[] = [
258
283
  id: 'deepseek/deepseek-r1:free',
259
284
  releasedAt: '2025-01-20',
260
285
  type: 'chat',
261
- },
286
+ },
262
287
  {
263
288
  abilities: {
264
289
  vision: true,
@@ -92,6 +92,39 @@ describe('LobeOpenRouterAI', () => {
92
92
  expect(result).toBeInstanceOf(Response);
93
93
  });
94
94
 
95
+ it('should add reasoning field when thinking is enabled', async () => {
96
+ // Arrange
97
+ const mockStream = new ReadableStream();
98
+ const mockResponse = Promise.resolve(mockStream);
99
+
100
+ (instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
101
+
102
+ // Act
103
+ const result = await instance.chat({
104
+ messages: [{ content: 'Hello', role: 'user' }],
105
+ model: 'mistralai/mistral-7b-instruct:free',
106
+ temperature: 0.7,
107
+ thinking: {
108
+ type: 'enabled',
109
+ budget_tokens: 1500,
110
+ },
111
+ });
112
+
113
+ // Assert
114
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ messages: [{ content: 'Hello', role: 'user' }],
117
+ model: 'mistralai/mistral-7b-instruct:free',
118
+ reasoning: {
119
+ max_tokens: 1500,
120
+ },
121
+ temperature: 0.7,
122
+ }),
123
+ { headers: { Accept: '*/*' } },
124
+ );
125
+ expect(result).toBeInstanceOf(Response);
126
+ });
127
+
95
128
  describe('Error', () => {
96
129
  it('should return OpenRouterBizError with an openai error response when OpenAI.APIError is thrown', async () => {
97
130
  // Arrange
@@ -2,7 +2,7 @@ import type { ChatModelCard } from '@/types/llm';
2
2
 
3
3
  import { ModelProvider } from '../types';
4
4
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
5
- import { OpenRouterModelCard, OpenRouterModelExtraInfo } from './type';
5
+ import { OpenRouterModelCard, OpenRouterModelExtraInfo, OpenRouterReasoning } from './type';
6
6
 
7
7
  const formatPrice = (price: string) => {
8
8
  if (price === '-1') return undefined;
@@ -13,10 +13,19 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
13
13
  baseURL: 'https://openrouter.ai/api/v1',
14
14
  chatCompletion: {
15
15
  handlePayload: (payload) => {
16
+ const { thinking } = payload;
17
+
18
+ let reasoning: OpenRouterReasoning = {};
19
+ if (thinking?.type === 'enabled') {
20
+ reasoning = {
21
+ max_tokens: thinking.budget_tokens,
22
+ };
23
+ }
24
+
16
25
  return {
17
26
  ...payload,
18
- include_reasoning: true,
19
27
  model: payload.enabledSearch ? `${payload.model}:online` : payload.model,
28
+ reasoning,
20
29
  stream: payload.stream ?? true,
21
30
  } as any;
22
31
  },
@@ -37,3 +37,22 @@ export interface OpenRouterModelExtraInfo {
37
37
  endpoint?: OpenRouterModelEndpoint;
38
38
  slug: string;
39
39
  }
40
+
41
+ interface OpenRouterOpenAIReasoning {
42
+ effort: 'high' | 'medium' | 'low';
43
+ exclude?: boolean;
44
+ }
45
+
46
+ interface OpenRouterAnthropicReasoning {
47
+ exclude?: boolean;
48
+ max_tokens: number;
49
+ }
50
+
51
+ interface OpenRouterCommonReasoning {
52
+ exclude?: boolean;
53
+ }
54
+
55
+ export type OpenRouterReasoning =
56
+ | OpenRouterOpenAIReasoning
57
+ | OpenRouterAnthropicReasoning
58
+ | OpenRouterCommonReasoning;
@@ -118,22 +118,23 @@ export const createModelListSlice: StateCreator<
118
118
  get().refreshModelProviderList({ trigger: 'refreshDefaultModelList' });
119
119
  },
120
120
  refreshModelProviderList: (params) => {
121
- const modelProviderList = get().defaultModelProviderList.map((list) => ({
122
- ...list,
123
- chatModels: modelProviderSelectors
124
- .getModelCardsById(list.id)(get())
125
- ?.map((model) => {
126
- const models = modelProviderSelectors.getEnableModelsById(list.id)(get());
127
-
128
- if (!models) return model;
129
-
130
- return {
131
- ...model,
132
- enabled: models?.some((m) => m === model.id),
133
- };
134
- }),
135
- enabled: modelProviderSelectors.isProviderEnabled(list.id as any)(get()),
136
- }));
121
+ const modelProviderList = get().defaultModelProviderList.map((list) => {
122
+ const enabledModels = modelProviderSelectors.getEnableModelsById(list.id)(get());
123
+ return {
124
+ ...list,
125
+ chatModels: modelProviderSelectors
126
+ .getModelCardsById(list.id)(get())
127
+ ?.map((model) => {
128
+ if (!enabledModels) return model;
129
+
130
+ return {
131
+ ...model,
132
+ enabled: enabledModels?.some((m) => m === model.id),
133
+ };
134
+ }),
135
+ enabled: modelProviderSelectors.isProviderEnabled(list.id as any)(get()),
136
+ };
137
+ });
137
138
 
138
139
  set({ modelProviderList }, false, `refreshModelList - ${params?.trigger}`);
139
140
  },