@lobehub/chat 1.94.4 → 1.94.6

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,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.94.6](https://github.com/lobehub/lobe-chat/compare/v1.94.5...v1.94.6)
6
+
7
+ <sup>Released on **2025-06-12**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Abort the Gemini request correctly & Add openai o3-pro.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Abort the Gemini request correctly & Add openai o3-pro, closes [#8135](https://github.com/lobehub/lobe-chat/issues/8135) ([c79f1b9](https://github.com/lobehub/lobe-chat/commit/c79f1b9))
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
+
30
+ ### [Version 1.94.5](https://github.com/lobehub/lobe-chat/compare/v1.94.4...v1.94.5)
31
+
32
+ <sup>Released on **2025-06-12**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **chat**: Improve response animation merging logic.
37
+
38
+ #### 💄 Styles
39
+
40
+ - **misc**: Support `web_search_preview` & fix some bug form OpenAI Response API.
41
+
42
+ <br/>
43
+
44
+ <details>
45
+ <summary><kbd>Improvements and Fixes</kbd></summary>
46
+
47
+ #### What's fixed
48
+
49
+ - **chat**: Improve response animation merging logic, closes [#8160](https://github.com/lobehub/lobe-chat/issues/8160) ([9d81cdc](https://github.com/lobehub/lobe-chat/commit/9d81cdc))
50
+
51
+ #### Styles
52
+
53
+ - **misc**: Support `web_search_preview` & fix some bug form OpenAI Response API, closes [#8131](https://github.com/lobehub/lobe-chat/issues/8131) ([b2983f0](https://github.com/lobehub/lobe-chat/commit/b2983f0))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ### [Version 1.94.4](https://github.com/lobehub/lobe-chat/compare/v1.94.3...v1.94.4)
6
64
 
7
65
  <sup>Released on **2025-06-11**</sup>
package/README.md CHANGED
@@ -335,7 +335,7 @@ In addition, these plugins are not limited to news aggregation, but can also ext
335
335
  | [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
336
336
  | [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
337
337
 
338
- > 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
338
+ > 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
339
339
 
340
340
  <!-- PLUGIN LIST -->
341
341
 
package/README.zh-CN.md CHANGED
@@ -328,7 +328,7 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
328
328
  | [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
329
329
  | [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
330
330
 
331
- > 📊 Total plugins: [<kbd>**43**</kbd>](https://lobechat.com/discover/plugins)
331
+ > 📊 Total plugins: [<kbd>**42**</kbd>](https://lobechat.com/discover/plugins)
332
332
 
333
333
  <!-- PLUGIN LIST -->
334
334
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Abort the Gemini request correctly & Add openai o3-pro."
6
+ ]
7
+ },
8
+ "date": "2025-06-12",
9
+ "version": "1.94.6"
10
+ },
11
+ {
12
+ "children": {
13
+ "improvements": [
14
+ "Support web_search_preview & fix some bug form OpenAI Response API."
15
+ ]
16
+ },
17
+ "date": "2025-06-12",
18
+ "version": "1.94.5"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.94.4",
3
+ "version": "1.94.6",
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",
@@ -54,7 +54,7 @@
54
54
  "dev:desktop": "next dev --turbopack -p 3015",
55
55
  "docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx && prettier -c --write locales/**/*",
56
56
  "docs:seo": "lobe-seo && npm run lint:mdx",
57
- "i18n": "npm run workflow:i18n && lobe-i18n",
57
+ "i18n": "npm run workflow:i18n && lobe-i18n && prettier -c --write \"locales/**\"",
58
58
  "lint": "npm run lint:ts && npm run lint:style && npm run type-check && npm run lint:circular",
59
59
  "lint:circular": "dpdm src/**/*.ts --no-warning --no-tree --exit-code circular:1 --no-progress -T true --skip-dynamic-imports circular",
60
60
  "lint:md": "remark . --silent --output",
@@ -282,6 +282,7 @@
282
282
  "@next/bundle-analyzer": "^15.3.3",
283
283
  "@next/eslint-plugin-next": "^15.3.3",
284
284
  "@peculiar/webcrypto": "^1.5.0",
285
+ "@prettier/sync": "^0.6.1",
285
286
  "@semantic-release/exec": "^6.0.3",
286
287
  "@testing-library/jest-dom": "^6.6.3",
287
288
  "@testing-library/react": "^16.3.0",
@@ -2,7 +2,7 @@ import { consola } from 'consola';
2
2
  import { colors } from 'consola/utils';
3
3
 
4
4
  import { entryLocaleJsonFilepath, i18nConfig, srcDefaultLocales } from './const';
5
- import { tagWhite, writeJSON } from './utils';
5
+ import { tagWhite, writeJSONWithPrettier } from './utils';
6
6
 
7
7
  export const genDefaultLocale = () => {
8
8
  consola.info(`Default locale is ${i18nConfig.entryLocale}...`);
@@ -13,7 +13,7 @@ export const genDefaultLocale = () => {
13
13
 
14
14
  for (const [ns, value] of data) {
15
15
  const filepath = entryLocaleJsonFilepath(`${ns}.json`);
16
- writeJSON(filepath, value);
16
+ writeJSONWithPrettier(filepath, value);
17
17
  consola.success(tagWhite(ns), colors.gray(filepath));
18
18
  }
19
19
  };
@@ -10,10 +10,10 @@ import {
10
10
  outputLocaleJsonFilepath,
11
11
  srcDefaultLocales,
12
12
  } from './const';
13
- import { readJSON, tagWhite, writeJSON } from './utils';
13
+ import { readJSON, tagWhite, writeJSONWithPrettier } from './utils';
14
14
 
15
15
  export const genDiff = () => {
16
- consola.start(`Diff between Dev/Prod local...`);
16
+ consola.start(`Remove diff analysis...`);
17
17
 
18
18
  const resources = require(srcDefaultLocales);
19
19
  const data = Object.entries(resources.default);
@@ -21,27 +21,26 @@ export const genDiff = () => {
21
21
  for (const [ns, devJSON] of data) {
22
22
  const filepath = entryLocaleJsonFilepath(`${ns}.json`);
23
23
  if (!existsSync(filepath)) continue;
24
- const prodJSON = readJSON(filepath);
24
+ const previousProdJSON = readJSON(filepath);
25
25
 
26
- const diffResult = diff(prodJSON, devJSON as any);
27
- const remove = diffResult.filter((item) => item.op === 'remove');
28
- if (remove.length === 0) {
26
+ const diffResult = diff(previousProdJSON, devJSON as any);
27
+ if (diffResult.length === 0) {
29
28
  consola.success(tagWhite(ns), colors.gray(filepath));
30
29
  continue;
31
30
  }
32
31
 
33
32
  const clearLocals = [];
34
33
 
35
- for (const locale of [i18nConfig.entryLocale, ...i18nConfig.outputLocales]) {
34
+ for (const locale of i18nConfig.outputLocales) {
36
35
  const localeFilepath = outputLocaleJsonFilepath(locale, `${ns}.json`);
37
36
  if (!existsSync(localeFilepath)) continue;
38
37
  const localeJSON = readJSON(localeFilepath);
39
38
 
40
- for (const item of remove) {
39
+ for (const item of diffResult) {
41
40
  unset(localeJSON, item.path);
42
41
  }
43
42
 
44
- writeJSON(localeFilepath, localeJSON);
43
+ writeJSONWithPrettier(localeFilepath, localeJSON);
45
44
  clearLocals.push(locale);
46
45
  }
47
46
  consola.info('clear', clearLocals);
@@ -2,9 +2,13 @@ import { consola } from 'consola';
2
2
  import { colors } from 'consola/utils';
3
3
  import { readFileSync, writeFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
-
5
+ import prettier from "@prettier/sync";
6
6
  import i18nConfig from '../../.i18nrc';
7
7
 
8
+ let prettierOptions = prettier.resolveConfig(
9
+ resolve(__dirname, '../../.prettierrc.js')
10
+ );
11
+
8
12
  export const readJSON = (filePath: string) => {
9
13
  const data = readFileSync(filePath, 'utf8');
10
14
  return JSON.parse(data);
@@ -15,6 +19,15 @@ export const writeJSON = (filePath: string, data: any) => {
15
19
  writeFileSync(filePath, jsonStr, 'utf8');
16
20
  };
17
21
 
22
+ export const writeJSONWithPrettier = (filePath: string, data: any) => {
23
+ const jsonStr = JSON.stringify(data, null, 2);
24
+ const formatted = prettier.format(jsonStr, {
25
+ ...prettierOptions,
26
+ parser: 'json',
27
+ });
28
+ writeFileSync(filePath, formatted, 'utf8');
29
+ };
30
+
18
31
  export const genResourcesContent = (locales: string[]) => {
19
32
  let index = '';
20
33
  let indexObj = '';
@@ -62,6 +62,20 @@ const groqChatModels: AIChatModelCard[] = [
62
62
  },
63
63
  type: 'chat',
64
64
  },
65
+ {
66
+ abilities: {
67
+ reasoning: true,
68
+ },
69
+ contextWindowTokens: 131_072,
70
+ displayName: 'Qwen3 32B',
71
+ id: 'qwen/qwen3-32b',
72
+ maxOutput: 16_384,
73
+ pricing: {
74
+ input: 0.29,
75
+ output: 0.59,
76
+ },
77
+ type: 'chat',
78
+ },
65
79
  {
66
80
  abilities: {
67
81
  functionCall: true,
@@ -8,6 +8,28 @@ import {
8
8
  } from '@/types/aiModel';
9
9
 
10
10
  export const openaiChatModels: AIChatModelCard[] = [
11
+ {
12
+ abilities: {
13
+ functionCall: true,
14
+ reasoning: true,
15
+ vision: true,
16
+ },
17
+ contextWindowTokens: 200_000,
18
+ description:
19
+ 'o3-pro 模型使用更多的计算来更深入地思考并始终提供更好的答案,仅支持 Responses API 下使用。',
20
+ displayName: 'o3-pro',
21
+ id: 'o3-pro',
22
+ maxOutput: 100_000,
23
+ pricing: {
24
+ input: 20,
25
+ output: 80,
26
+ },
27
+ releasedAt: '2025-06-10',
28
+ settings: {
29
+ extendParams: ['reasoningEffort'],
30
+ },
31
+ type: 'chat',
32
+ },
11
33
  {
12
34
  abilities: {
13
35
  functionCall: true,
@@ -22,11 +44,11 @@ export const openaiChatModels: AIChatModelCard[] = [
22
44
  id: 'o3',
23
45
  maxOutput: 100_000,
24
46
  pricing: {
25
- cachedInput: 2.5,
26
- input: 10,
27
- output: 40,
47
+ cachedInput: 0.5,
48
+ input: 2,
49
+ output: 8,
28
50
  },
29
- releasedAt: '2025-04-17',
51
+ releasedAt: '2025-04-16',
30
52
  settings: {
31
53
  extendParams: ['reasoningEffort'],
32
54
  },
@@ -59,6 +81,7 @@ export const openaiChatModels: AIChatModelCard[] = [
59
81
  {
60
82
  abilities: {
61
83
  functionCall: true,
84
+ search: true,
62
85
  vision: true,
63
86
  },
64
87
  contextWindowTokens: 1_047_576,
@@ -73,11 +96,15 @@ export const openaiChatModels: AIChatModelCard[] = [
73
96
  output: 8,
74
97
  },
75
98
  releasedAt: '2025-04-14',
99
+ settings: {
100
+ searchImpl: 'params',
101
+ },
76
102
  type: 'chat',
77
103
  },
78
104
  {
79
105
  abilities: {
80
106
  functionCall: true,
107
+ search: true,
81
108
  vision: true,
82
109
  },
83
110
  contextWindowTokens: 1_047_576,
@@ -93,6 +120,9 @@ export const openaiChatModels: AIChatModelCard[] = [
93
120
  output: 1.6,
94
121
  },
95
122
  releasedAt: '2025-04-14',
123
+ settings: {
124
+ searchImpl: 'params',
125
+ },
96
126
  type: 'chat',
97
127
  },
98
128
  {
@@ -135,6 +165,28 @@ export const openaiChatModels: AIChatModelCard[] = [
135
165
  },
136
166
  type: 'chat',
137
167
  },
168
+ {
169
+ abilities: {
170
+ functionCall: true,
171
+ reasoning: true,
172
+ vision: true,
173
+ },
174
+ contextWindowTokens: 200_000,
175
+ description:
176
+ 'o1 系列模型经过强化学习训练,能够在回答前进行思考,并执行复杂的推理任务。o1-pro 模型使用了更多计算资源,以进行更深入的思考,从而持续提供更优质的回答。',
177
+ displayName: 'o1-pro',
178
+ id: 'o1-pro',
179
+ maxOutput: 100_000,
180
+ pricing: {
181
+ input: 150,
182
+ output: 600,
183
+ },
184
+ releasedAt: '2025-03-19',
185
+ settings: {
186
+ extendParams: ['reasoningEffort'],
187
+ },
188
+ type: 'chat',
189
+ },
138
190
  {
139
191
  abilities: {
140
192
  reasoning: true,
@@ -158,6 +210,7 @@ export const openaiChatModels: AIChatModelCard[] = [
158
210
  },
159
211
  {
160
212
  abilities: {
213
+ functionCall: true,
161
214
  reasoning: true,
162
215
  vision: true,
163
216
  },
@@ -220,6 +273,7 @@ export const openaiChatModels: AIChatModelCard[] = [
220
273
  {
221
274
  abilities: {
222
275
  functionCall: true,
276
+ search: true,
223
277
  vision: true,
224
278
  },
225
279
  contextWindowTokens: 128_000,
@@ -234,6 +288,9 @@ export const openaiChatModels: AIChatModelCard[] = [
234
288
  output: 0.6,
235
289
  },
236
290
  releasedAt: '2024-07-18',
291
+ settings: {
292
+ searchImpl: 'params',
293
+ },
237
294
  type: 'chat',
238
295
  },
239
296
  {
@@ -259,6 +316,29 @@ export const openaiChatModels: AIChatModelCard[] = [
259
316
  {
260
317
  abilities: {
261
318
  functionCall: true,
319
+ //search: true,
320
+ },
321
+ contextWindowTokens: 128_000,
322
+ description: 'GPT-4o mini Audio 模型,支持音频输入输出',
323
+ displayName: 'GPT-4o mini Audio',
324
+ id: 'gpt-4o-mini-audio-preview',
325
+ maxOutput: 16_384,
326
+ pricing: {
327
+ input: 0.15,
328
+ output: 0.6,
329
+ },
330
+ releasedAt: '2024-12-17',
331
+ /*
332
+ settings: {
333
+ searchImpl: 'params',
334
+ },
335
+ */
336
+ type: 'chat',
337
+ },
338
+ {
339
+ abilities: {
340
+ functionCall: true,
341
+ search: true,
262
342
  vision: true,
263
343
  },
264
344
  contextWindowTokens: 128_000,
@@ -272,6 +352,9 @@ export const openaiChatModels: AIChatModelCard[] = [
272
352
  output: 10,
273
353
  },
274
354
  releasedAt: '2024-05-13',
355
+ settings: {
356
+ searchImpl: 'params',
357
+ },
275
358
  type: 'chat',
276
359
  },
277
360
  {
@@ -297,6 +380,7 @@ export const openaiChatModels: AIChatModelCard[] = [
297
380
  {
298
381
  abilities: {
299
382
  functionCall: true,
383
+ search: true,
300
384
  vision: true,
301
385
  },
302
386
  contextWindowTokens: 128_000,
@@ -310,11 +394,15 @@ export const openaiChatModels: AIChatModelCard[] = [
310
394
  output: 10,
311
395
  },
312
396
  releasedAt: '2024-11-20',
397
+ settings: {
398
+ searchImpl: 'params',
399
+ },
313
400
  type: 'chat',
314
401
  },
315
402
  {
316
403
  abilities: {
317
404
  functionCall: true,
405
+ search: true,
318
406
  vision: true,
319
407
  },
320
408
  contextWindowTokens: 128_000,
@@ -327,9 +415,16 @@ export const openaiChatModels: AIChatModelCard[] = [
327
415
  output: 15,
328
416
  },
329
417
  releasedAt: '2024-05-13',
418
+ settings: {
419
+ searchImpl: 'params',
420
+ },
330
421
  type: 'chat',
331
422
  },
332
423
  {
424
+ abilities: {
425
+ functionCall: true,
426
+ //search: true,
427
+ },
333
428
  contextWindowTokens: 128_000,
334
429
  description: 'GPT-4o Audio 模型,支持音频输入输出',
335
430
  displayName: 'GPT-4o Audio',
@@ -340,6 +435,11 @@ export const openaiChatModels: AIChatModelCard[] = [
340
435
  output: 10,
341
436
  },
342
437
  releasedAt: '2024-10-01',
438
+ /*
439
+ settings: {
440
+ searchImpl: 'params',
441
+ },
442
+ */
343
443
  type: 'chat',
344
444
  },
345
445
  {
@@ -545,6 +645,48 @@ export const openaiChatModels: AIChatModelCard[] = [
545
645
  },
546
646
  type: 'chat',
547
647
  },
648
+ {
649
+ abilities: {
650
+ functionCall: true,
651
+ reasoning: true,
652
+ vision: true,
653
+ },
654
+ contextWindowTokens: 200_000,
655
+ description: 'codex-mini-latest 是 o4-mini 的微调版本,专门用于 Codex CLI。对于直接通过 API 使用,我们推荐从 gpt-4.1 开始。',
656
+ displayName: 'Codex mini',
657
+ id: 'codex-mini-latest',
658
+ maxOutput: 100_000,
659
+ pricing: {
660
+ input: 1.5,
661
+ output: 6,
662
+ },
663
+ releasedAt: '2025-06-01',
664
+ settings: {
665
+ extendParams: ['reasoningEffort'],
666
+ },
667
+ type: 'chat',
668
+ },
669
+ {
670
+ abilities: {
671
+ functionCall: true,
672
+ reasoning: true,
673
+ vision: true,
674
+ },
675
+ contextWindowTokens: 8192,
676
+ description: 'computer-use-preview 模型是专为“计算机使用工具”设计的专用模型,经过训练以理解并执行计算机相关任务。',
677
+ displayName: 'Computer Use Preview',
678
+ id: 'computer-use-preview',
679
+ maxOutput: 1024,
680
+ pricing: {
681
+ input: 3,
682
+ output: 12,
683
+ },
684
+ releasedAt: '2025-03-11',
685
+ settings: {
686
+ extendParams: ['reasoningEffort'],
687
+ },
688
+ type: 'chat',
689
+ },
548
690
  ];
549
691
 
550
692
  export const openaiEmbeddingModels: AIEmbeddingModelCard[] = [
@@ -323,7 +323,7 @@ const OpenAI: ModelProviderCard = {
323
323
  },
324
324
  },
325
325
  ],
326
- checkModel: 'gpt-4o-mini',
326
+ checkModel: 'gpt-4.1-nano',
327
327
  description:
328
328
  'OpenAI 是全球领先的人工智能研究机构,其开发的模型如GPT系列推动了自然语言处理的前沿。OpenAI 致力于通过创新和高效的AI解决方案改变多个行业。他们的产品具有显著的性能和经济性,广泛用于研究、商业和创新应用。',
329
329
  enabled: true,
@@ -6,7 +6,32 @@ export const systemToUserModels = new Set([
6
6
  ]);
7
7
 
8
8
  // TODO: 临时写法,后续要重构成 model card 展示配置
9
- export const disableStreamModels = new Set(['o1', 'o1-2024-12-17']);
9
+ export const disableStreamModels = new Set([
10
+ 'o1',
11
+ 'o1-2024-12-17',
12
+ 'o1-pro',
13
+ 'o1-pro-2025-03-19',
14
+ /*
15
+ 官网显示不支持,但是实际试下来支持 Streaming,暂时注释掉
16
+ 'o3-pro',
17
+ 'o3-pro-2025-06-10',
18
+ */
19
+ 'computer-use-preview',
20
+ 'computer-use-preview-2025-03-11',
21
+ ]);
22
+
23
+ /**
24
+ * models use Responses API only
25
+ */
26
+ export const responsesAPIModels = new Set([
27
+ 'o1-pro',
28
+ 'o1-pro-2025-03-19',
29
+ 'o3-pro',
30
+ 'o3-pro-2025-06-10',
31
+ 'codex-mini-latest',
32
+ 'computer-use-preview',
33
+ 'computer-use-preview-2025-03-11',
34
+ ]);
10
35
 
11
36
  /**
12
37
  * models support context caching
@@ -25,11 +25,7 @@ import {
25
25
  import { AgentRuntimeError } from '../utils/createError';
26
26
  import { debugStream } from '../utils/debugStream';
27
27
  import { StreamingResponse } from '../utils/response';
28
- import {
29
- GoogleGenerativeAIStream,
30
- VertexAIStream,
31
- convertIterableToStream,
32
- } from '../utils/streams';
28
+ import { GoogleGenerativeAIStream, VertexAIStream } from '../utils/streams';
33
29
  import { parseDataUri } from '../utils/uriParser';
34
30
 
35
31
  const modelsOffSafetySettings = new Set(['gemini-2.0-flash-exp']);
@@ -91,6 +87,17 @@ interface GoogleAIThinkingConfig {
91
87
  thinkingBudget?: number;
92
88
  }
93
89
 
90
+ const isAbortError = (error: Error): boolean => {
91
+ const message = error.message.toLowerCase();
92
+ return (
93
+ message.includes('aborted') ||
94
+ message.includes('cancelled') ||
95
+ message.includes('error reading from the stream') ||
96
+ message.includes('abort') ||
97
+ error.name === 'AbortError'
98
+ );
99
+ };
100
+
94
101
  export class LobeGoogleAI implements LobeRuntimeAI {
95
102
  private client: GoogleGenerativeAI;
96
103
  private isVertexAi: boolean;
@@ -140,6 +147,20 @@ export class LobeGoogleAI implements LobeRuntimeAI {
140
147
  const contents = await this.buildGoogleMessages(payload.messages);
141
148
 
142
149
  const inputStartAt = Date.now();
150
+
151
+ const controller = new AbortController();
152
+ const originalSignal = options?.signal;
153
+
154
+ if (originalSignal) {
155
+ if (originalSignal.aborted) {
156
+ controller.abort();
157
+ } else {
158
+ originalSignal.addEventListener('abort', () => {
159
+ controller.abort();
160
+ });
161
+ }
162
+ }
163
+
143
164
  const geminiStreamResult = await this.client
144
165
  .getGenerativeModel(
145
166
  {
@@ -177,15 +198,20 @@ export class LobeGoogleAI implements LobeRuntimeAI {
177
198
  },
178
199
  { apiVersion: 'v1beta', baseUrl: this.baseURL },
179
200
  )
180
- .generateContentStream({
181
- contents,
182
- systemInstruction: modelsDisableInstuction.has(model)
183
- ? undefined
184
- : (payload.system as string),
185
- tools: this.buildGoogleTools(payload.tools, payload),
186
- });
201
+ .generateContentStream(
202
+ {
203
+ contents,
204
+ systemInstruction: modelsDisableInstuction.has(model)
205
+ ? undefined
206
+ : (payload.system as string),
207
+ tools: this.buildGoogleTools(payload.tools, payload),
208
+ },
209
+ {
210
+ signal: controller.signal,
211
+ },
212
+ );
187
213
 
188
- const googleStream = convertIterableToStream(geminiStreamResult.stream);
214
+ const googleStream = this.createEnhancedStream(geminiStreamResult.stream, controller.signal);
189
215
  const [prod, useForDebug] = googleStream.tee();
190
216
 
191
217
  const key = this.isVertexAi
@@ -205,6 +231,16 @@ export class LobeGoogleAI implements LobeRuntimeAI {
205
231
  } catch (e) {
206
232
  const err = e as Error;
207
233
 
234
+ // 移除之前的静默处理,统一抛出错误
235
+ if (isAbortError(err)) {
236
+ console.log('Request was cancelled');
237
+ throw AgentRuntimeError.chat({
238
+ error: { message: 'Request was cancelled' },
239
+ errorType: AgentRuntimeErrorType.ProviderBizError,
240
+ provider: this.provider,
241
+ });
242
+ }
243
+
208
244
  console.log(err);
209
245
  const { errorType, error } = this.parseErrorMessage(err.message);
210
246
 
@@ -212,24 +248,75 @@ export class LobeGoogleAI implements LobeRuntimeAI {
212
248
  }
213
249
  }
214
250
 
215
- async models() {
251
+ private createEnhancedStream(originalStream: any, signal: AbortSignal): ReadableStream {
252
+ return new ReadableStream({
253
+ async start(controller) {
254
+ let hasData = false;
255
+
256
+ try {
257
+ for await (const chunk of originalStream) {
258
+ if (signal.aborted) {
259
+ // 如果有数据已经输出,优雅地关闭流而不是抛出错误
260
+ if (hasData) {
261
+ console.log('Stream cancelled gracefully, preserving existing output');
262
+ controller.close();
263
+ return;
264
+ } else {
265
+ // 如果还没有数据输出,则抛出取消错误
266
+ throw new Error('Stream cancelled');
267
+ }
268
+ }
269
+
270
+ hasData = true;
271
+ controller.enqueue(chunk);
272
+ }
273
+ } catch (error) {
274
+ const err = error as Error;
275
+
276
+ // 统一处理所有错误,包括 abort 错误
277
+ if (isAbortError(err) || signal.aborted) {
278
+ // 如果有数据已经输出,优雅地关闭流
279
+ if (hasData) {
280
+ console.log('Stream reading cancelled gracefully, preserving existing output');
281
+ controller.close();
282
+ return;
283
+ } else {
284
+ console.log('Stream reading cancelled before any output');
285
+ controller.error(new Error('Stream cancelled'));
286
+ return;
287
+ }
288
+ } else {
289
+ // 处理其他流解析错误
290
+ console.error('Stream parsing error:', err);
291
+ controller.error(err);
292
+ return;
293
+ }
294
+ }
295
+
296
+ controller.close();
297
+ },
298
+ });
299
+ }
300
+
301
+ async models(options?: { signal?: AbortSignal }) {
216
302
  try {
217
303
  const url = `${this.baseURL}/v1beta/models?key=${this.apiKey}`;
218
304
  const response = await fetch(url, {
219
305
  method: 'GET',
306
+ signal: options?.signal,
220
307
  });
221
-
308
+
222
309
  if (!response.ok) {
223
310
  throw new Error(`HTTP error! status: ${response.status}`);
224
311
  }
225
-
312
+
226
313
  const json = await response.json();
227
-
314
+
228
315
  const modelList: GoogleModelCard[] = json.models;
229
-
316
+
230
317
  const processedModels = modelList.map((model) => {
231
318
  const id = model.name.replace(/^models\//, '');
232
-
319
+
233
320
  return {
234
321
  contextWindowTokens: (model.inputTokenLimit || 0) + (model.outputTokenLimit || 0),
235
322
  displayName: model.displayName || id,
@@ -237,9 +324,9 @@ export class LobeGoogleAI implements LobeRuntimeAI {
237
324
  maxOutput: model.outputTokenLimit || undefined,
238
325
  };
239
326
  });
240
-
327
+
241
328
  const { MODEL_LIST_CONFIGS, processModelList } = await import('../utils/modelParse');
242
-
329
+
243
330
  return processModelList(processedModels, MODEL_LIST_CONFIGS.google);
244
331
  } catch (error) {
245
332
  console.error('Failed to fetch Google models:', error);
@@ -2,21 +2,24 @@ import { ChatStreamPayload, ModelProvider } from '../types';
2
2
  import { processMultiProviderModelList } from '../utils/modelParse';
3
3
  import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
4
4
  import { pruneReasoningPayload } from '../utils/openaiHelpers';
5
+ import { responsesAPIModels } from '@/const/models';
5
6
 
6
7
  export interface OpenAIModelCard {
7
8
  id: string;
8
9
  }
9
10
 
10
- const prunePrefixes = ['o1', 'o3', 'o4'];
11
+ const prunePrefixes = ['o1', 'o3', 'o4', 'codex', 'computer-use'];
12
+
13
+ const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
11
14
 
12
15
  export const LobeOpenAI = createOpenAICompatibleRuntime({
13
16
  baseURL: 'https://api.openai.com/v1',
14
17
  chatCompletion: {
15
18
  handlePayload: (payload) => {
16
- const { model } = payload;
19
+ const { enabledSearch, model, ...rest } = payload;
17
20
 
18
- if (model === 'o1-pro') {
19
- return { ...payload, apiMode: 'responses' } as ChatStreamPayload;
21
+ if (responsesAPIModels.has(model) || enabledSearch) {
22
+ return { ...rest, apiMode: 'responses', enabledSearch, model } as ChatStreamPayload;
20
23
  }
21
24
 
22
25
  if (prunePrefixes.some((prefix) => model.startsWith(prefix))) {
@@ -24,11 +27,10 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
24
27
  }
25
28
 
26
29
  if (model.includes('-search-')) {
27
- const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
28
-
29
30
  return {
30
- ...payload,
31
+ ...rest,
31
32
  frequency_penalty: undefined,
33
+ model,
32
34
  presence_penalty: undefined,
33
35
  stream: payload.stream ?? true,
34
36
  temperature: undefined,
@@ -41,7 +43,7 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
41
43
  } as any;
42
44
  }
43
45
 
44
- return { ...payload, stream: payload.stream ?? true };
46
+ return { ...rest, model, stream: payload.stream ?? true };
45
47
  },
46
48
  },
47
49
  debug: {
@@ -57,17 +59,37 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
57
59
  },
58
60
  provider: ModelProvider.OpenAI,
59
61
  responses: {
60
- handlePayload: (payload: ChatStreamPayload) => {
61
- const { model } = payload;
62
+ handlePayload: (payload) => {
63
+ const { enabledSearch, model, tools, ...rest } = payload;
64
+
65
+ const openaiTools = enabledSearch
66
+ ? [
67
+ ...(tools || []),
68
+ {
69
+ type: 'web_search_preview',
70
+ ...(oaiSearchContextSize && {
71
+ search_context_size: oaiSearchContextSize,
72
+ }),
73
+ },
74
+ ]
75
+ : tools;
76
+
62
77
  if (prunePrefixes.some((prefix) => model.startsWith(prefix))) {
63
78
  if (!payload.reasoning) {
64
79
  payload.reasoning = { summary: 'auto' };
65
80
  } else {
66
81
  payload.reasoning.summary = 'auto';
67
82
  }
83
+
84
+ // computer-use series must set truncation as auto
85
+ if (model.startsWith('computer-use')) {
86
+ payload.truncation = 'auto';
87
+ }
88
+
89
+ return pruneReasoningPayload(payload) as any;
68
90
  }
69
91
 
70
- return { ...payload, stream: payload.stream ?? true };
92
+ return { ...rest, model, stream: payload.stream ?? true, tools: openaiTools } as any;
71
93
  },
72
94
  },
73
95
  });
@@ -107,6 +107,7 @@ export interface ChatStreamPayload {
107
107
  effort?: string;
108
108
  summary?: string;
109
109
  };
110
+ reasoning_effort?: 'low' | 'medium' | 'high';
110
111
  responseMode?: 'stream' | 'json';
111
112
  /**
112
113
  * @title 是否开启流式请求
@@ -132,6 +133,7 @@ export interface ChatStreamPayload {
132
133
  * @default 1
133
134
  */
134
135
  top_p?: number;
136
+ truncation?: 'auto' | 'disabled';
135
137
  }
136
138
 
137
139
  export interface ChatMethodOptions {
@@ -209,14 +209,9 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
209
209
  }
210
210
 
211
211
  async chat(
212
- { responseMode, apiMode, ...payload }: ChatStreamPayload,
212
+ { responseMode, ...payload }: ChatStreamPayload,
213
213
  options?: ChatMethodOptions,
214
214
  ) {
215
- // new openai Response API
216
- if (apiMode === 'responses') {
217
- return this.handleResponseAPIMode(payload, options);
218
- }
219
-
220
215
  try {
221
216
  const inputStartAt = Date.now();
222
217
  const postPayload = chatCompletion?.handlePayload
@@ -226,6 +221,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
226
221
  stream: payload.stream ?? true,
227
222
  } as OpenAI.ChatCompletionCreateParamsStreaming);
228
223
 
224
+ // new openai Response API
225
+ if ((postPayload as any).apiMode === 'responses') {
226
+ return this.handleResponseAPIMode(payload, options);
227
+ }
228
+
229
229
  const messages = await convertOpenAIMessages(postPayload.messages);
230
230
 
231
231
  let response: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
@@ -478,11 +478,12 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
478
478
  ): Promise<Response> {
479
479
  const inputStartAt = Date.now();
480
480
 
481
- const { messages, ...res } = responses?.handlePayload
481
+ const { messages, reasoning_effort, tools, ...res } = responses?.handlePayload
482
482
  ? (responses?.handlePayload(payload, this._options) as ChatStreamPayload)
483
483
  : payload;
484
484
 
485
485
  // remove penalty params
486
+ delete res.apiMode;
486
487
  delete res.frequency_penalty;
487
488
  delete res.presence_penalty;
488
489
 
@@ -490,9 +491,10 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
490
491
 
491
492
  const postPayload = {
492
493
  ...res,
494
+ ...(reasoning_effort ? { reasoning: { effort: reasoning_effort } } : {}),
493
495
  input,
494
496
  store: false,
495
- tools: payload.tools?.map((tool) => this.convertChatCompletionToolToResponseTool(tool)),
497
+ tools: tools?.map((tool) => this.convertChatCompletionToolToResponseTool(tool)),
496
498
  } as OpenAI.Responses.ResponseCreateParamsStreaming;
497
499
 
498
500
  if (debug?.responses?.()) {
@@ -86,11 +86,11 @@ exports[`OpenAIResponsesStream > Reasoning > summary 1`] = `
86
86
  "data: " analyzing"
87
87
 
88
88
  ",
89
- "id: resp_684313b89200819087f27686e0c822260b502bf083132d0d
89
+ "id: rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d
90
90
  ",
91
- "event: data
91
+ "event: text
92
92
  ",
93
- "data: {"type":"response.output_item.done","output_index":0,"item":{"id":"rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d","type":"reasoning","summary":[{"type":"summary_text","text":"**Answering a numeric comparison**\\n\\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it's greater than 9.1. We can respond with \\"9.92大于9.1\\" without needing to search for more information. It's simple comparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with \\"9.92 > 9.1\\" is perfectly fine!"}]}}
93
+ "data: null
94
94
 
95
95
  ",
96
96
  "id: resp_684313b89200819087f27686e0c822260b502bf083132d0d
@@ -128,11 +128,11 @@ exports[`OpenAIResponsesStream > Reasoning > summary 1`] = `
128
128
  "data: {"type":"response.content_part.done","item_id":"msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"text":"9.92 比 9.1 大。"}}
129
129
 
130
130
  ",
131
- "id: resp_684313b89200819087f27686e0c822260b502bf083132d0d
131
+ "id: msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d
132
132
  ",
133
- "event: data
133
+ "event: text
134
134
  ",
135
- "data: {"type":"response.output_item.done","output_index":1,"item":{"id":"msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"9.92 比 9. 大。"}],"role":"assistant"}}
135
+ "data: null
136
136
 
137
137
  ",
138
138
  "id: resp_684313b89200819087f27686e0c822260b502bf083132d0d
@@ -1,7 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  import type { Stream } from 'openai/streaming';
3
3
 
4
- import { ChatMessageError } from '@/types/message';
4
+ import { ChatMessageError, CitationItem } from '@/types/message';
5
5
 
6
6
  import { AgentRuntimeErrorType } from '../../../error';
7
7
  import { convertResponseUsage } from '../../usageConverter';
@@ -20,7 +20,17 @@ import {
20
20
  import { OpenAIStreamOptions } from './openai';
21
21
 
22
22
  const transformOpenAIStream = (
23
- chunk: OpenAI.Responses.ResponseStreamEvent,
23
+ chunk: OpenAI.Responses.ResponseStreamEvent | {
24
+ annotation: {
25
+ end_index: number;
26
+ start_index: number;
27
+ title: string;
28
+ type: 'url_citation';
29
+ url: string;
30
+ };
31
+ item_id: string;
32
+ type: 'response.output_text.annotation.added';
33
+ },
24
34
  streamContext: StreamContext,
25
35
  ): StreamProtocolChunk | StreamProtocolChunk[] => {
26
36
  // handle the first chunk error
@@ -42,6 +52,7 @@ const transformOpenAIStream = (
42
52
  switch (chunk.type) {
43
53
  case 'response.created': {
44
54
  streamContext.id = chunk.response.id;
55
+ streamContext.returnedCitationArray = [];
45
56
 
46
57
  return { data: chunk.response.status, id: streamContext.id, type: 'data' };
47
58
  }
@@ -106,6 +117,31 @@ const transformOpenAIStream = (
106
117
  return { data: chunk.delta, id: chunk.item_id, type: 'reasoning' };
107
118
  }
108
119
 
120
+ case 'response.output_text.annotation.added': {
121
+ const citations = chunk.annotation;
122
+
123
+ if (streamContext.returnedCitationArray) {
124
+ streamContext.returnedCitationArray.push({
125
+ title: citations.title,
126
+ url: citations.url,
127
+ } as CitationItem);
128
+ }
129
+
130
+ return { data: null, id: chunk.item_id, type: 'text' };
131
+ }
132
+
133
+ case 'response.output_item.done': {
134
+ if (streamContext.returnedCitationArray?.length) {
135
+ return {
136
+ data: { citations: streamContext.returnedCitationArray },
137
+ id: chunk.item.id,
138
+ type: 'grounding',
139
+ }
140
+ }
141
+
142
+ return { data: null, id: chunk.item.id, type: 'text' };
143
+ }
144
+
109
145
  case 'response.completed': {
110
146
  if (chunk.response.usage) {
111
147
  return {
@@ -388,6 +388,13 @@ class ChatService {
388
388
  const userPreferTransitionMode =
389
389
  userGeneralSettingsSelectors.transitionMode(getUserStoreState());
390
390
 
391
+ // The order of the array is very important.
392
+ const mergedResponseAnimation = [
393
+ providerConfig?.settings?.responseAnimation || {},
394
+ userPreferTransitionMode,
395
+ responseAnimation,
396
+ ].reduce((acc, cur) => merge(acc, standardizeAnimationStyle(cur)), {});
397
+
391
398
  return fetchSSE(API_ENDPOINTS.chat(sdkType), {
392
399
  body: JSON.stringify(payload),
393
400
  fetcher: fetcher,
@@ -397,10 +404,7 @@ class ChatService {
397
404
  onErrorHandle: options?.onErrorHandle,
398
405
  onFinish: options?.onFinish,
399
406
  onMessageHandle: options?.onMessageHandle,
400
- responseAnimation: [userPreferTransitionMode, responseAnimation].reduce(
401
- (acc, cur) => merge(acc, standardizeAnimationStyle(cur)),
402
- providerConfig?.settings?.responseAnimation ?? {},
403
- ),
407
+ responseAnimation: mergedResponseAnimation,
404
408
  signal,
405
409
  });
406
410
  };