@lobehub/chat 1.68.2 → 1.68.4

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/usage/providers/azureai.mdx +69 -0
  4. package/docs/usage/providers/azureai.zh-CN.mdx +69 -0
  5. package/docs/usage/providers/deepseek.mdx +3 -3
  6. package/docs/usage/providers/deepseek.zh-CN.mdx +5 -4
  7. package/docs/usage/providers/jina.mdx +51 -0
  8. package/docs/usage/providers/jina.zh-CN.mdx +51 -0
  9. package/docs/usage/providers/lmstudio.mdx +75 -0
  10. package/docs/usage/providers/lmstudio.zh-CN.mdx +75 -0
  11. package/docs/usage/providers/nvidia.mdx +55 -0
  12. package/docs/usage/providers/nvidia.zh-CN.mdx +55 -0
  13. package/docs/usage/providers/ppio.mdx +7 -7
  14. package/docs/usage/providers/ppio.zh-CN.mdx +6 -6
  15. package/docs/usage/providers/sambanova.mdx +50 -0
  16. package/docs/usage/providers/sambanova.zh-CN.mdx +50 -0
  17. package/docs/usage/providers/tencentcloud.mdx +49 -0
  18. package/docs/usage/providers/tencentcloud.zh-CN.mdx +49 -0
  19. package/docs/usage/providers/vertexai.mdx +59 -0
  20. package/docs/usage/providers/vertexai.zh-CN.mdx +59 -0
  21. package/docs/usage/providers/vllm.mdx +98 -0
  22. package/docs/usage/providers/vllm.zh-CN.mdx +98 -0
  23. package/docs/usage/providers/volcengine.mdx +47 -0
  24. package/docs/usage/providers/volcengine.zh-CN.mdx +48 -0
  25. package/locales/ar/chat.json +29 -0
  26. package/locales/ar/models.json +48 -0
  27. package/locales/ar/providers.json +3 -0
  28. package/locales/bg-BG/chat.json +29 -0
  29. package/locales/bg-BG/models.json +48 -0
  30. package/locales/bg-BG/providers.json +3 -0
  31. package/locales/de-DE/chat.json +29 -0
  32. package/locales/de-DE/models.json +48 -0
  33. package/locales/de-DE/providers.json +3 -0
  34. package/locales/en-US/chat.json +29 -0
  35. package/locales/en-US/models.json +48 -0
  36. package/locales/en-US/providers.json +3 -3
  37. package/locales/es-ES/chat.json +29 -0
  38. package/locales/es-ES/models.json +48 -0
  39. package/locales/es-ES/providers.json +3 -0
  40. package/locales/fa-IR/chat.json +29 -0
  41. package/locales/fa-IR/models.json +48 -0
  42. package/locales/fa-IR/providers.json +3 -0
  43. package/locales/fr-FR/chat.json +29 -0
  44. package/locales/fr-FR/models.json +48 -0
  45. package/locales/fr-FR/providers.json +3 -0
  46. package/locales/it-IT/chat.json +29 -0
  47. package/locales/it-IT/models.json +48 -0
  48. package/locales/it-IT/providers.json +3 -0
  49. package/locales/ja-JP/chat.json +29 -0
  50. package/locales/ja-JP/models.json +48 -0
  51. package/locales/ja-JP/providers.json +3 -0
  52. package/locales/ko-KR/chat.json +29 -0
  53. package/locales/ko-KR/models.json +48 -0
  54. package/locales/ko-KR/providers.json +3 -0
  55. package/locales/nl-NL/chat.json +29 -0
  56. package/locales/nl-NL/models.json +48 -0
  57. package/locales/nl-NL/providers.json +3 -0
  58. package/locales/pl-PL/chat.json +29 -0
  59. package/locales/pl-PL/models.json +48 -0
  60. package/locales/pl-PL/providers.json +3 -0
  61. package/locales/pt-BR/chat.json +29 -0
  62. package/locales/pt-BR/models.json +48 -0
  63. package/locales/pt-BR/providers.json +3 -0
  64. package/locales/ru-RU/chat.json +29 -0
  65. package/locales/ru-RU/models.json +48 -0
  66. package/locales/ru-RU/providers.json +3 -0
  67. package/locales/tr-TR/chat.json +29 -0
  68. package/locales/tr-TR/models.json +48 -0
  69. package/locales/tr-TR/providers.json +3 -0
  70. package/locales/vi-VN/chat.json +29 -0
  71. package/locales/vi-VN/models.json +48 -0
  72. package/locales/vi-VN/providers.json +3 -0
  73. package/locales/zh-CN/chat.json +29 -0
  74. package/locales/zh-CN/models.json +51 -3
  75. package/locales/zh-CN/providers.json +3 -4
  76. package/locales/zh-TW/chat.json +29 -0
  77. package/locales/zh-TW/models.json +48 -0
  78. package/locales/zh-TW/providers.json +3 -0
  79. package/package.json +1 -1
  80. package/packages/web-crawler/src/crawImpl/__test__/jina.test.ts +169 -0
  81. package/packages/web-crawler/src/crawImpl/jina.ts +1 -1
  82. package/packages/web-crawler/src/crawImpl/naive.ts +29 -3
  83. package/packages/web-crawler/src/urlRules.ts +7 -1
  84. package/packages/web-crawler/src/utils/errorType.ts +7 -0
  85. package/scripts/serverLauncher/startServer.js +11 -7
  86. package/src/config/modelProviders/ppio.ts +1 -1
  87. package/src/features/Conversation/Extras/Assistant.tsx +12 -20
  88. package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +130 -0
  89. package/src/features/Conversation/Extras/Usage/UsageDetail/TokenProgress.tsx +71 -0
  90. package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +146 -0
  91. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +94 -0
  92. package/src/features/Conversation/Extras/Usage/index.tsx +40 -0
  93. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +14 -0
  94. package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -0
  95. package/src/libs/agent-runtime/utils/streams/openai.test.ts +100 -10
  96. package/src/libs/agent-runtime/utils/streams/openai.ts +30 -4
  97. package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
  98. package/src/locales/default/chat.ts +30 -1
  99. package/src/server/routers/tools/search.ts +1 -1
  100. package/src/store/aiInfra/slices/aiModel/initialState.ts +3 -1
  101. package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
  102. package/src/store/aiInfra/slices/aiModel/selectors.ts +5 -0
  103. package/src/store/aiInfra/slices/aiProvider/action.ts +3 -1
  104. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
  105. package/src/store/chat/slices/message/action.ts +3 -0
  106. package/src/store/global/initialState.ts +1 -0
  107. package/src/store/global/selectors/systemStatus.ts +2 -0
  108. package/src/types/message/base.ts +18 -0
  109. package/src/types/message/chat.ts +4 -3
  110. package/src/utils/fetch/fetchSSE.ts +24 -1
  111. package/src/utils/format.ts +3 -1
@@ -222,6 +222,10 @@ describe('AnthropicStream', () => {
222
222
  'id: msg_017aTuY86wNxth5TE544yqJq',
223
223
  'event: stop',
224
224
  'data: "tool_use"\n',
225
+
226
+ 'id: msg_017aTuY86wNxth5TE544yqJq',
227
+ 'event: usage',
228
+ 'data: {"inputTokens":457,"outputTokens":84,"totalTokens":541}\n',
225
229
  ].map((item) => `${item}\n`),
226
230
  );
227
231
 
@@ -375,6 +379,10 @@ describe('AnthropicStream', () => {
375
379
  'event: stop',
376
380
  'data: "tool_use"\n',
377
381
 
382
+ 'id: msg_0175ryA67RbGrnRrGBXFQEYK',
383
+ 'event: usage',
384
+ 'data: {"inputTokens":485,"outputTokens":154,"totalTokens":639}\n',
385
+
378
386
  'id: msg_0175ryA67RbGrnRrGBXFQEYK',
379
387
  'event: stop',
380
388
  'data: "message_stop"\n',
@@ -506,6 +514,9 @@ describe('AnthropicStream', () => {
506
514
  'event: stop',
507
515
  'data: "end_turn"\n',
508
516
  'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
517
+ 'event: usage',
518
+ 'data: {"inputTokens":46,"outputTokens":365,"totalTokens":411}\n',
519
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
509
520
  'event: stop',
510
521
  'data: "message_stop"\n',
511
522
  ].map((item) => `${item}\n`),
@@ -663,6 +674,9 @@ describe('AnthropicStream', () => {
663
674
  'event: stop',
664
675
  'data: "end_turn"\n',
665
676
  'id: msg_019q32esPvu3TftzZnL6JPys',
677
+ 'event: usage',
678
+ 'data: {"inputTokens":92,"outputTokens":263,"totalTokens":355}\n',
679
+ 'id: msg_019q32esPvu3TftzZnL6JPys',
666
680
  'event: stop',
667
681
  'data: "message_stop"\n',
668
682
  ].map((item) => `${item}\n`),
@@ -1,6 +1,8 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import type { Stream } from '@anthropic-ai/sdk/streaming';
3
3
 
4
+ import { ModelTokensUsage } from '@/types/message';
5
+
4
6
  import { ChatStreamCallbacks } from '../../types';
5
7
  import {
6
8
  StreamContext,
@@ -20,6 +22,11 @@ export const transformAnthropicStream = (
20
22
  switch (chunk.type) {
21
23
  case 'message_start': {
22
24
  context.id = chunk.message.id;
25
+ context.usage = {
26
+ inputTokens: chunk.message.usage?.input_tokens,
27
+ outputTokens: chunk.message.usage?.output_tokens,
28
+ };
29
+
23
30
  return { data: chunk.message, id: chunk.message.id, type: 'data' };
24
31
  }
25
32
  case 'content_block_start': {
@@ -133,6 +140,24 @@ export const transformAnthropicStream = (
133
140
  }
134
141
 
135
142
  case 'message_delta': {
143
+ const outputTokens = chunk.usage?.output_tokens + (context.usage?.outputTokens || 0);
144
+ const inputTokens = context.usage?.inputTokens || 0;
145
+ const totalTokens = inputTokens + outputTokens;
146
+
147
+ if (totalTokens > 0) {
148
+ return [
149
+ { data: chunk.delta.stop_reason, id: context.id, type: 'stop' },
150
+ {
151
+ data: {
152
+ inputTokens: inputTokens,
153
+ outputTokens: outputTokens,
154
+ totalTokens: inputTokens + outputTokens,
155
+ } as ModelTokensUsage,
156
+ id: context.id,
157
+ type: 'usage',
158
+ },
159
+ ];
160
+ }
136
161
  return { data: chunk.delta.stop_reason, id: context.id, type: 'stop' };
137
162
  }
138
163
 
@@ -348,6 +348,96 @@ describe('OpenAIStream', () => {
348
348
  ]);
349
349
  });
350
350
 
351
+ it('should streaming token usage', async () => {
352
+ const data = [
353
+ {
354
+ id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
355
+ object: 'chat.completion.chunk',
356
+ created: 1741056525,
357
+ model: 'gpt-4o-mini-2024-07-18',
358
+ choices: [{ index: 0, delta: { role: 'assistant', content: '' } }],
359
+ service_tier: 'default',
360
+ system_fingerprint: 'fp_06737a9306',
361
+ },
362
+ {
363
+ id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
364
+ object: 'chat.completion.chunk',
365
+ created: 1741056525,
366
+ model: 'gpt-4o-mini-2024-07-18',
367
+ choices: [{ index: 0, delta: { content: '你好!' } }],
368
+ service_tier: 'default',
369
+ system_fingerprint: 'fp_06737a9306',
370
+ },
371
+ {
372
+ id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
373
+ object: 'chat.completion.chunk',
374
+ created: 1741056525,
375
+ model: 'gpt-4o-mini-2024-07-18',
376
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
377
+ service_tier: 'default',
378
+ system_fingerprint: 'fp_06737a9306',
379
+ },
380
+ {
381
+ id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
382
+ object: 'chat.completion.chunk',
383
+ created: 1741056525,
384
+ model: 'gpt-4o-mini-2024-07-18',
385
+ choices: [],
386
+ service_tier: 'default',
387
+ system_fingerprint: 'fp_06737a9306',
388
+ usage: {
389
+ prompt_tokens: 1646,
390
+ completion_tokens: 11,
391
+ total_tokens: 1657,
392
+ prompt_tokens_details: { audio_tokens: 0, cached_tokens: 0 },
393
+ completion_tokens_details: {
394
+ accepted_prediction_tokens: 0,
395
+ audio_tokens: 0,
396
+ reasoning_tokens: 0,
397
+ rejected_prediction_tokens: 0,
398
+ },
399
+ },
400
+ },
401
+ ];
402
+
403
+ const mockOpenAIStream = new ReadableStream({
404
+ start(controller) {
405
+ data.forEach((chunk) => {
406
+ controller.enqueue(chunk);
407
+ });
408
+
409
+ controller.close();
410
+ },
411
+ });
412
+
413
+ const protocolStream = OpenAIStream(mockOpenAIStream);
414
+
415
+ const decoder = new TextDecoder();
416
+ const chunks = [];
417
+
418
+ // @ts-ignore
419
+ for await (const chunk of protocolStream) {
420
+ chunks.push(decoder.decode(chunk, { stream: true }));
421
+ }
422
+
423
+ expect(chunks).toEqual(
424
+ [
425
+ 'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
426
+ 'event: text',
427
+ `data: ""\n`,
428
+ 'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
429
+ 'event: text',
430
+ `data: "你好!"\n`,
431
+ 'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
432
+ 'event: stop',
433
+ `data: "stop"\n`,
434
+ 'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
435
+ 'event: usage',
436
+ `data: {"acceptedPredictionTokens":0,"cachedTokens":0,"inputAudioTokens":0,"inputTokens":1646,"outputAudioTokens":0,"outputTokens":11,"reasoningTokens":0,"rejectedPredictionTokens":0,"totalTokens":1657}\n`,
437
+ ].map((i) => `${i}\n`),
438
+ );
439
+ });
440
+
351
441
  describe('Tools Calling', () => {
352
442
  it('should handle OpenAI official tool calls', async () => {
353
443
  const mockOpenAIStream = new ReadableStream({
@@ -749,8 +839,8 @@ describe('OpenAIStream', () => {
749
839
  'event: text',
750
840
  `data: "帮助。"\n`,
751
841
  'id: 1',
752
- 'event: stop',
753
- `data: "stop"\n`,
842
+ 'event: usage',
843
+ `data: {"cachedTokens":0,"inputCacheMissTokens":6,"inputTokens":6,"outputTokens":104,"reasoningTokens":70,"totalTokens":110}\n`,
754
844
  ].map((i) => `${i}\n`),
755
845
  );
756
846
  });
@@ -968,8 +1058,8 @@ describe('OpenAIStream', () => {
968
1058
  'event: text',
969
1059
  `data: "帮助。"\n`,
970
1060
  'id: 1',
971
- 'event: stop',
972
- `data: "stop"\n`,
1061
+ 'event: usage',
1062
+ `data: {"cachedTokens":0,"inputCacheMissTokens":6,"inputTokens":6,"outputTokens":104,"reasoningTokens":70,"totalTokens":110}\n`,
973
1063
  ].map((i) => `${i}\n`),
974
1064
  );
975
1065
  });
@@ -1169,8 +1259,8 @@ describe('OpenAIStream', () => {
1169
1259
  'event: text',
1170
1260
  `data: "帮助。"\n`,
1171
1261
  'id: 1',
1172
- 'event: stop',
1173
- `data: "stop"\n`,
1262
+ 'event: usage',
1263
+ `data: {"cachedTokens":0,"inputCacheMissTokens":6,"inputTokens":6,"outputTokens":104,"reasoningTokens":70,"totalTokens":110}\n`,
1174
1264
  ].map((i) => `${i}\n`),
1175
1265
  );
1176
1266
  });
@@ -1370,8 +1460,8 @@ describe('OpenAIStream', () => {
1370
1460
  'event: text',
1371
1461
  `data: "帮助。"\n`,
1372
1462
  'id: 1',
1373
- 'event: stop',
1374
- `data: "stop"\n`,
1463
+ 'event: usage',
1464
+ `data: {"cachedTokens":0,"inputCacheMissTokens":6,"inputTokens":6,"outputTokens":104,"reasoningTokens":70,"totalTokens":110}\n`,
1375
1465
  ].map((i) => `${i}\n`),
1376
1466
  );
1377
1467
  });
@@ -1571,8 +1661,8 @@ describe('OpenAIStream', () => {
1571
1661
  'event: text',
1572
1662
  `data: "帮助。"\n`,
1573
1663
  'id: 1',
1574
- 'event: stop',
1575
- `data: "stop"\n`,
1664
+ 'event: usage',
1665
+ `data: {"cachedTokens":0,"inputCacheMissTokens":6,"inputTokens":6,"outputTokens":104,"reasoningTokens":70,"totalTokens":110}\n`,
1576
1666
  ].map((i) => `${i}\n`),
1577
1667
  );
1578
1668
  });
@@ -1,7 +1,7 @@
1
1
  import OpenAI from 'openai';
2
2
  import type { Stream } from 'openai/streaming';
3
3
 
4
- import { ChatMessageError, CitationItem } from '@/types/message';
4
+ import { ChatMessageError, CitationItem, ModelTokensUsage } from '@/types/message';
5
5
 
6
6
  import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../../error';
7
7
  import { ChatStreamCallbacks } from '../../types';
@@ -18,6 +18,22 @@ import {
18
18
  generateToolCallId,
19
19
  } from './protocol';
20
20
 
21
+ const convertUsage = (usage: OpenAI.Completions.CompletionUsage): ModelTokensUsage => {
22
+ return {
23
+ acceptedPredictionTokens: usage.completion_tokens_details?.accepted_prediction_tokens,
24
+ cachedTokens:
25
+ (usage as any).prompt_cache_hit_tokens || usage.prompt_tokens_details?.cached_tokens,
26
+ inputAudioTokens: usage.prompt_tokens_details?.audio_tokens,
27
+ inputCacheMissTokens: (usage as any).prompt_cache_miss_tokens,
28
+ inputTokens: usage.prompt_tokens,
29
+ outputAudioTokens: usage.completion_tokens_details?.audio_tokens,
30
+ outputTokens: usage.completion_tokens,
31
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens,
32
+ rejectedPredictionTokens: usage.completion_tokens_details?.rejected_prediction_tokens,
33
+ totalTokens: usage.total_tokens,
34
+ };
35
+ };
36
+
21
37
  export const transformOpenAIStream = (
22
38
  chunk: OpenAI.ChatCompletionChunk,
23
39
  streamContext: StreamContext,
@@ -41,11 +57,16 @@ export const transformOpenAIStream = (
41
57
  // maybe need another structure to add support for multiple choices
42
58
  const item = chunk.choices[0];
43
59
  if (!item) {
60
+ if (chunk.usage) {
61
+ const usage = chunk.usage;
62
+ return { data: convertUsage(usage), id: chunk.id, type: 'usage' };
63
+ }
64
+
44
65
  return { data: chunk, id: chunk.id, type: 'data' };
45
66
  }
46
67
 
47
- // tools calling
48
- if (typeof item.delta?.tool_calls === 'object' && item.delta.tool_calls?.length > 0) {
68
+ if (item && typeof item.delta?.tool_calls === 'object' && item.delta.tool_calls?.length > 0) {
69
+ // tools calling
49
70
  const tool_calls = item.delta.tool_calls.filter(
50
71
  (value) => value.index >= 0 || typeof value.index === 'undefined',
51
72
  );
@@ -97,6 +118,11 @@ export const transformOpenAIStream = (
97
118
  return { data: item.delta.content, id: chunk.id, type: 'text' };
98
119
  }
99
120
 
121
+ if (chunk.usage) {
122
+ const usage = chunk.usage;
123
+ return { data: convertUsage(usage), id: chunk.id, type: 'usage' };
124
+ }
125
+
100
126
  return { data: item.finish_reason, id: chunk.id, type: 'stop' };
101
127
  }
102
128
 
@@ -147,7 +173,7 @@ export const transformOpenAIStream = (
147
173
  ({
148
174
  title: typeof item === 'string' ? item : item.title,
149
175
  url: typeof item === 'string' ? item : item.url,
150
- }) as CitationItem
176
+ }) as CitationItem,
151
177
  ),
152
178
  },
153
179
  id: chunk.id,
@@ -1,4 +1,5 @@
1
1
  import { ChatStreamCallbacks } from '@/libs/agent-runtime';
2
+ import { ModelTokensUsage } from '@/types/message';
2
3
 
3
4
  import { AgentRuntimeErrorType } from '../../error';
4
5
 
@@ -23,6 +24,7 @@ export interface StreamContext {
23
24
  name: string;
24
25
  };
25
26
  toolIndex?: number;
27
+ usage?: ModelTokensUsage;
26
28
  }
27
29
 
28
30
  export interface StreamProtocolChunk {
@@ -44,6 +46,8 @@ export interface StreamProtocolChunk {
44
46
  | 'stop'
45
47
  // Error
46
48
  | 'error'
49
+ // token usage
50
+ | 'usage'
47
51
  // unknown data result
48
52
  | 'data';
49
53
  }
@@ -81,6 +81,36 @@ export default {
81
81
  deleteDisabledByThreads: '存在子话题,不能删除',
82
82
  regenerate: '重新生成',
83
83
  },
84
+ messages: {
85
+ modelCard: {
86
+ credit: '积分',
87
+ creditPricing: '定价',
88
+ creditTooltip:
89
+ '为便于计数,我们将 1$ 折算为 1M 积分,例如 $3/M tokens 即可折算为 3积分/token',
90
+ pricing: {
91
+ inputCachedTokens: '缓存输入 {{amount}}/积分 · ${{amount}}/M',
92
+ inputCharts: '${{amount}}/M 字符',
93
+ inputMinutes: '${{amount}}/分钟',
94
+ inputTokens: '输入 {{amount}}/积分 · ${{amount}}/M',
95
+ outputTokens: '输出 {{amount}}/积分 · ${{amount}}/M',
96
+ },
97
+ },
98
+ tokenDetails: {
99
+ input: '输入',
100
+ inputAudio: '音频输入',
101
+ inputCached: '输入缓存',
102
+ inputText: '文本输入',
103
+ inputTitle: '输入明细',
104
+ inputUncached: '输入未缓存',
105
+ output: '输出',
106
+ outputAudio: '音频输出',
107
+ outputText: '文本输出',
108
+ outputTitle: '输出明细',
109
+ reasoning: '深度思考',
110
+ title: '生成明细',
111
+ total: '总计消耗',
112
+ },
113
+ },
84
114
  newAgent: '新建助手',
85
115
  pin: '置顶',
86
116
  pinOff: '取消置顶',
@@ -194,7 +224,6 @@ export default {
194
224
  action: '语音朗读',
195
225
  clear: '删除语音',
196
226
  },
197
-
198
227
  updateAgent: '更新助理信息',
199
228
  upload: {
200
229
  action: {
@@ -27,7 +27,7 @@ export const searchRouter = router({
27
27
  async (url) => {
28
28
  return await crawler.crawl({ impls: input.impls, url });
29
29
  },
30
- { concurrency: 10 },
30
+ { concurrency: 3 },
31
31
  );
32
32
 
33
33
  return { results };
@@ -1,8 +1,9 @@
1
- import { AiProviderModelListItem } from '@/types/aiModel';
1
+ import { AiProviderModelListItem, LobeDefaultAiModelListItem } from '@/types/aiModel';
2
2
 
3
3
  export interface AIModelsState {
4
4
  aiModelLoadingIds: string[];
5
5
  aiProviderModelList: AiProviderModelListItem[];
6
+ builtinAiModelList: LobeDefaultAiModelListItem[];
6
7
  isAiModelListInit?: boolean;
7
8
  modelSearchKeyword: string;
8
9
  }
@@ -10,5 +11,6 @@ export interface AIModelsState {
10
11
  export const initialAIModelState: AIModelsState = {
11
12
  aiModelLoadingIds: [],
12
13
  aiProviderModelList: [],
14
+ builtinAiModelList: [],
13
15
  modelSearchKeyword: '',
14
16
  };
@@ -34,6 +34,7 @@ describe('aiModelSelectors', () => {
34
34
  displayName: 'Remote Model',
35
35
  },
36
36
  ],
37
+ builtinAiModelList: [],
37
38
  modelSearchKeyword: '',
38
39
  aiModelLoadingIds: ['model2'],
39
40
  enabledAiModels: [
@@ -22,8 +22,12 @@ const filteredAiProviderModelList = (s: AIProviderStoreState) => {
22
22
  };
23
23
 
24
24
  const totalAiProviderModelList = (s: AIProviderStoreState) => s.aiProviderModelList.length;
25
+
25
26
  const isEmptyAiProviderModelList = (s: AIProviderStoreState) => totalAiProviderModelList(s) === 0;
26
27
 
28
+ const getModelCard = (model: string, provider: string) => (s: AIProviderStoreState) =>
29
+ s.builtinAiModelList.find((item) => item.id === model && item.providerId === provider);
30
+
27
31
  const hasRemoteModels = (s: AIProviderStoreState) =>
28
32
  s.aiProviderModelList.some((m) => m.source === AiModelSourceEnum.Remote);
29
33
 
@@ -113,6 +117,7 @@ export const aiModelSelectors = {
113
117
  filteredAiProviderModelList,
114
118
  getAiModelById,
115
119
  getEnabledModelById,
120
+ getModelCard,
116
121
  hasRemoteModels,
117
122
  isEmptyAiProviderModelList,
118
123
  isModelEnabled,
@@ -184,7 +184,7 @@ export const createAiProviderSlice: StateCreator<
184
184
  };
185
185
  },
186
186
  {
187
- onSuccess: (data) => {
187
+ onSuccess: async (data) => {
188
188
  if (!data) return;
189
189
 
190
190
  const getModelListByType = (providerId: string, type: string) => {
@@ -206,10 +206,12 @@ export const createAiProviderSlice: StateCreator<
206
206
  children: getModelListByType(provider.id, 'chat'),
207
207
  name: provider.name || provider.id,
208
208
  }));
209
+ const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
209
210
 
210
211
  set(
211
212
  {
212
213
  aiProviderRuntimeConfig: data.runtimeConfig,
214
+ builtinAiModelList: LOBE_DEFAULT_MODEL_LIST,
213
215
  enabledAiModels: data.enabledAiModels,
214
216
  enabledAiProviders: data.enabledAiProviders,
215
217
  enabledChatModelList,
@@ -455,7 +455,10 @@ export const generateAIChat: StateCreator<
455
455
  await messageService.updateMessageError(messageId, error);
456
456
  await refreshMessages();
457
457
  },
458
- onFinish: async (content, { traceId, observationId, toolCalls, reasoning, grounding }) => {
458
+ onFinish: async (
459
+ content,
460
+ { traceId, observationId, toolCalls, reasoning, grounding, usage },
461
+ ) => {
459
462
  // if there is traceId, update it
460
463
  if (traceId) {
461
464
  msgTraceId = traceId;
@@ -474,6 +477,7 @@ export const generateAIChat: StateCreator<
474
477
  toolCalls,
475
478
  reasoning: !!reasoning ? { ...reasoning, duration } : undefined,
476
479
  search: !!grounding?.citations ? grounding : undefined,
480
+ metadata: usage,
477
481
  });
478
482
  },
479
483
  onMessageHandle: async (chunk) => {
@@ -17,6 +17,7 @@ import {
17
17
  ChatMessageError,
18
18
  ChatMessagePluginError,
19
19
  CreateMessageParams,
20
+ MessageMetadata,
20
21
  MessageToolCall,
21
22
  ModelReasoning,
22
23
  } from '@/types/message';
@@ -79,6 +80,7 @@ export interface ChatMessageAction {
79
80
  toolCalls?: MessageToolCall[];
80
81
  reasoning?: ModelReasoning;
81
82
  search?: GroundingSearch;
83
+ metadata?: MessageMetadata;
82
84
  },
83
85
  ) => Promise<void>;
84
86
  /**
@@ -308,6 +310,7 @@ export const chatMessage: StateCreator<
308
310
  tools: extra?.toolCalls ? internal_transformToolCalls(extra?.toolCalls) : undefined,
309
311
  reasoning: extra?.reasoning,
310
312
  search: extra?.search,
313
+ metadata: extra?.metadata,
311
314
  });
312
315
  await refreshMessages();
313
316
  },
@@ -50,6 +50,7 @@ export interface SystemStatus {
50
50
  * 应用初始化时不启用 PGLite,只有当用户手动开启时才启用
51
51
  */
52
52
  isEnablePglite?: boolean;
53
+ isShowCredit?: boolean;
53
54
  language?: LocaleMode;
54
55
  latestChangelogId?: string;
55
56
  mobileShowPortal?: boolean;
@@ -16,6 +16,7 @@ const showChatSideBar = (s: GlobalStore) => !s.status.zenMode && s.status.showCh
16
16
  const showSessionPanel = (s: GlobalStore) => !s.status.zenMode && s.status.showSessionPanel;
17
17
  const showFilePanel = (s: GlobalStore) => s.status.showFilePanel;
18
18
  const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
19
+ const isShowCredit = (s: GlobalStore) => s.status.isShowCredit;
19
20
 
20
21
  const showChatHeader = (s: GlobalStore) => !s.status.zenMode;
21
22
  const inZenMode = (s: GlobalStore) => s.status.zenMode;
@@ -58,6 +59,7 @@ export const systemStatusSelectors = {
58
59
  isPgliteInited,
59
60
  isPgliteNotEnabled,
60
61
  isPgliteNotInited,
62
+ isShowCredit,
61
63
  mobileShowPortal,
62
64
  mobileShowTopic,
63
65
  portalWidth,
@@ -13,6 +13,23 @@ export interface ModelReasoning {
13
13
  signature?: string;
14
14
  }
15
15
 
16
+ export interface ModelTokensUsage {
17
+ acceptedPredictionTokens?: number;
18
+ cachedTokens?: number;
19
+ inputAudioTokens?: number;
20
+ inputCacheMissTokens?: number;
21
+ inputTokens?: number;
22
+ outputAudioTokens?: number;
23
+ outputTokens?: number;
24
+ reasoningTokens?: number;
25
+ rejectedPredictionTokens?: number;
26
+ totalTokens?: number;
27
+ }
28
+
29
+ export interface MessageMetadata extends ModelTokensUsage {
30
+ tps?: number;
31
+ }
32
+
16
33
  export type MessageRoleType = 'user' | 'system' | 'assistant' | 'tool';
17
34
 
18
35
  export interface MessageItem {
@@ -23,6 +40,7 @@ export interface MessageItem {
23
40
  error: any | null;
24
41
  favorite: boolean | null;
25
42
  id: string;
43
+ metadata?: MessageMetadata | null;
26
44
  model: string | null;
27
45
  observationId: string | null;
28
46
  parentId: string | null;
@@ -6,7 +6,7 @@ import { MetaData } from '@/types/meta';
6
6
  import { MessageSemanticSearchChunk } from '@/types/rag';
7
7
  import { GroundingSearch } from '@/types/search';
8
8
 
9
- import { MessageRoleType, ModelReasoning } from './base';
9
+ import { MessageMetadata, MessageRoleType, ModelReasoning } from './base';
10
10
  import { ChatPluginPayload, ChatToolPayload } from './tools';
11
11
  import { Translate } from './translate';
12
12
 
@@ -82,15 +82,16 @@ export interface ChatMessage {
82
82
  imageList?: ChatImageItem[];
83
83
  meta: MetaData;
84
84
 
85
+ metadata?: MessageMetadata | null;
85
86
  /**
86
87
  * observation id
87
88
  */
88
89
  observationId?: string;
90
+
89
91
  /**
90
92
  * parent message id
91
93
  */
92
94
  parentId?: string;
93
-
94
95
  plugin?: ChatPluginPayload;
95
96
  pluginError?: any;
96
97
  pluginState?: any;
@@ -100,8 +101,8 @@ export interface ChatMessage {
100
101
  quotaId?: string;
101
102
  ragQuery?: string | null;
102
103
  ragQueryId?: string | null;
103
- ragRawQuery?: string | null;
104
104
 
105
+ ragRawQuery?: string | null;
105
106
  reasoning?: ModelReasoning | null;
106
107
  /**
107
108
  * message role type
@@ -10,6 +10,7 @@ import {
10
10
  MessageToolCallChunk,
11
11
  MessageToolCallSchema,
12
12
  ModelReasoning,
13
+ ModelTokensUsage,
13
14
  } from '@/types/message';
14
15
  import { GroundingSearch } from '@/types/search';
15
16
 
@@ -28,9 +29,15 @@ export type OnFinishHandler = (
28
29
  toolCalls?: MessageToolCall[];
29
30
  traceId?: string | null;
30
31
  type?: SSEFinishType;
32
+ usage?: ModelTokensUsage;
31
33
  },
32
34
  ) => Promise<void>;
33
35
 
36
+ export interface MessageUsageChunk {
37
+ type: 'usage';
38
+ usage: ModelTokensUsage;
39
+ }
40
+
34
41
  export interface MessageTextChunk {
35
42
  text: string;
36
43
  type: 'text';
@@ -59,7 +66,12 @@ export interface FetchSSEOptions {
59
66
  onErrorHandle?: (error: ChatMessageError) => void;
60
67
  onFinish?: OnFinishHandler;
61
68
  onMessageHandle?: (
62
- chunk: MessageTextChunk | MessageToolCallsChunk | MessageReasoningChunk | MessageGroundingChunk,
69
+ chunk:
70
+ | MessageTextChunk
71
+ | MessageToolCallsChunk
72
+ | MessageReasoningChunk
73
+ | MessageGroundingChunk
74
+ | MessageUsageChunk,
63
75
  ) => void;
64
76
  smoothing?: SmoothingParams | boolean;
65
77
  }
@@ -317,6 +329,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
317
329
  });
318
330
 
319
331
  let grounding: GroundingSearch | undefined = undefined;
332
+ let usage: ModelTokensUsage | undefined = undefined;
320
333
  await fetchEventSource(url, {
321
334
  body: options.body,
322
335
  fetch: options?.fetcher,
@@ -377,6 +390,9 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
377
390
  }
378
391
 
379
392
  case 'text': {
393
+ // skip empty text
394
+ if (!data) break;
395
+
380
396
  if (textSmoothing) {
381
397
  textController.pushToQueue(data);
382
398
 
@@ -389,6 +405,12 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
389
405
  break;
390
406
  }
391
407
 
408
+ case 'usage': {
409
+ usage = data;
410
+ options.onMessageHandle?.({ type: 'usage', usage: data });
411
+ break;
412
+ }
413
+
392
414
  case 'grounding': {
393
415
  grounding = data;
394
416
  options.onMessageHandle?.({ grounding: data, type: 'grounding' });
@@ -475,6 +497,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
475
497
  toolCalls,
476
498
  traceId,
477
499
  type: finishedType,
500
+ usage,
478
501
  });
479
502
  }
480
503
  }