@lobehub/chat 1.68.3 → 1.68.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +14 -17
  4. package/changelog/v1.json +18 -0
  5. package/docs/usage/providers/azureai.mdx +69 -0
  6. package/docs/usage/providers/azureai.zh-CN.mdx +69 -0
  7. package/docs/usage/providers/deepseek.mdx +3 -3
  8. package/docs/usage/providers/deepseek.zh-CN.mdx +5 -4
  9. package/docs/usage/providers/jina.mdx +51 -0
  10. package/docs/usage/providers/jina.zh-CN.mdx +51 -0
  11. package/docs/usage/providers/lmstudio.mdx +75 -0
  12. package/docs/usage/providers/lmstudio.zh-CN.mdx +75 -0
  13. package/docs/usage/providers/nvidia.mdx +55 -0
  14. package/docs/usage/providers/nvidia.zh-CN.mdx +55 -0
  15. package/docs/usage/providers/ppio.mdx +7 -7
  16. package/docs/usage/providers/ppio.zh-CN.mdx +6 -6
  17. package/docs/usage/providers/sambanova.mdx +50 -0
  18. package/docs/usage/providers/sambanova.zh-CN.mdx +50 -0
  19. package/docs/usage/providers/tencentcloud.mdx +49 -0
  20. package/docs/usage/providers/tencentcloud.zh-CN.mdx +49 -0
  21. package/docs/usage/providers/vertexai.mdx +59 -0
  22. package/docs/usage/providers/vertexai.zh-CN.mdx +59 -0
  23. package/docs/usage/providers/vllm.mdx +98 -0
  24. package/docs/usage/providers/vllm.zh-CN.mdx +98 -0
  25. package/docs/usage/providers/volcengine.mdx +47 -0
  26. package/docs/usage/providers/volcengine.zh-CN.mdx +48 -0
  27. package/locales/ar/chat.json +29 -0
  28. package/locales/ar/models.json +48 -0
  29. package/locales/ar/providers.json +3 -0
  30. package/locales/bg-BG/chat.json +29 -0
  31. package/locales/bg-BG/models.json +48 -0
  32. package/locales/bg-BG/providers.json +3 -0
  33. package/locales/de-DE/chat.json +29 -0
  34. package/locales/de-DE/models.json +48 -0
  35. package/locales/de-DE/providers.json +3 -0
  36. package/locales/en-US/chat.json +29 -0
  37. package/locales/en-US/models.json +48 -0
  38. package/locales/en-US/providers.json +3 -3
  39. package/locales/es-ES/chat.json +29 -0
  40. package/locales/es-ES/models.json +48 -0
  41. package/locales/es-ES/providers.json +3 -0
  42. package/locales/fa-IR/chat.json +29 -0
  43. package/locales/fa-IR/models.json +48 -0
  44. package/locales/fa-IR/providers.json +3 -0
  45. package/locales/fr-FR/chat.json +29 -0
  46. package/locales/fr-FR/models.json +48 -0
  47. package/locales/fr-FR/providers.json +3 -0
  48. package/locales/it-IT/chat.json +29 -0
  49. package/locales/it-IT/models.json +48 -0
  50. package/locales/it-IT/providers.json +3 -0
  51. package/locales/ja-JP/chat.json +29 -0
  52. package/locales/ja-JP/models.json +48 -0
  53. package/locales/ja-JP/providers.json +3 -0
  54. package/locales/ko-KR/chat.json +29 -0
  55. package/locales/ko-KR/models.json +48 -0
  56. package/locales/ko-KR/providers.json +3 -0
  57. package/locales/nl-NL/chat.json +29 -0
  58. package/locales/nl-NL/models.json +48 -0
  59. package/locales/nl-NL/providers.json +3 -0
  60. package/locales/pl-PL/chat.json +29 -0
  61. package/locales/pl-PL/models.json +48 -0
  62. package/locales/pl-PL/providers.json +3 -0
  63. package/locales/pt-BR/chat.json +29 -0
  64. package/locales/pt-BR/models.json +48 -0
  65. package/locales/pt-BR/providers.json +3 -0
  66. package/locales/ru-RU/chat.json +29 -0
  67. package/locales/ru-RU/models.json +48 -0
  68. package/locales/ru-RU/providers.json +3 -0
  69. package/locales/tr-TR/chat.json +29 -0
  70. package/locales/tr-TR/models.json +48 -0
  71. package/locales/tr-TR/providers.json +3 -0
  72. package/locales/vi-VN/chat.json +29 -0
  73. package/locales/vi-VN/models.json +48 -0
  74. package/locales/vi-VN/providers.json +3 -0
  75. package/locales/zh-CN/chat.json +29 -0
  76. package/locales/zh-CN/models.json +51 -3
  77. package/locales/zh-CN/providers.json +3 -4
  78. package/locales/zh-TW/chat.json +29 -0
  79. package/locales/zh-TW/models.json +48 -0
  80. package/locales/zh-TW/providers.json +3 -0
  81. package/package.json +1 -1
  82. package/packages/web-crawler/src/crawImpl/__test__/jina.test.ts +169 -0
  83. package/packages/web-crawler/src/crawImpl/naive.ts +29 -3
  84. package/packages/web-crawler/src/utils/errorType.ts +7 -0
  85. package/scripts/serverLauncher/startServer.js +11 -7
  86. package/src/config/modelProviders/index.ts +1 -1
  87. package/src/config/modelProviders/ppio.ts +1 -1
  88. package/src/features/Conversation/Extras/Assistant.tsx +12 -20
  89. package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +130 -0
  90. package/src/features/Conversation/Extras/Usage/UsageDetail/TokenProgress.tsx +71 -0
  91. package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +146 -0
  92. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +94 -0
  93. package/src/features/Conversation/Extras/Usage/index.tsx +40 -0
  94. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +14 -0
  95. package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -0
  96. package/src/libs/agent-runtime/utils/streams/openai.test.ts +100 -10
  97. package/src/libs/agent-runtime/utils/streams/openai.ts +30 -4
  98. package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
  99. package/src/locales/default/chat.ts +30 -1
  100. package/src/server/routers/tools/search.ts +1 -1
  101. package/src/store/aiInfra/slices/aiModel/initialState.ts +3 -1
  102. package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
  103. package/src/store/aiInfra/slices/aiModel/selectors.ts +5 -0
  104. package/src/store/aiInfra/slices/aiProvider/action.ts +3 -1
  105. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
  106. package/src/store/chat/slices/message/action.ts +3 -0
  107. package/src/store/global/initialState.ts +1 -0
  108. package/src/store/global/selectors/systemStatus.ts +2 -0
  109. package/src/types/message/base.ts +18 -0
  110. package/src/types/message/chat.ts +4 -3
  111. package/src/utils/fetch/fetchSSE.ts +24 -1
  112. package/src/utils/format.ts +3 -1
@@ -0,0 +1,94 @@
1
+ import { LobeDefaultAiModelListItem } from '@/types/aiModel';
2
+ import { ModelTokensUsage } from '@/types/message';
3
+
4
+ const calcCredit = (token: number, pricing?: number) => {
5
+ if (!pricing) return '-';
6
+
7
+ return parseInt((token * pricing).toFixed(0));
8
+ };
9
+
10
+ export const getDetailsToken = (
11
+ usage: ModelTokensUsage,
12
+ modelCard?: LobeDefaultAiModelListItem,
13
+ ) => {
14
+ const uncachedInputCredit = (
15
+ !!usage.inputTokens
16
+ ? calcCredit(usage.inputTokens - (usage.cachedTokens || 0), modelCard?.pricing?.input)
17
+ : 0
18
+ ) as number;
19
+
20
+ const cachedInputCredit = (
21
+ !!usage.cachedTokens ? calcCredit(usage.cachedTokens, modelCard?.pricing?.cachedInput) : 0
22
+ ) as number;
23
+
24
+ const totalOutput = (
25
+ !!usage.outputTokens ? calcCredit(usage.outputTokens, modelCard?.pricing?.output) : 0
26
+ ) as number;
27
+
28
+ const totalTokens = uncachedInputCredit + cachedInputCredit + totalOutput;
29
+ return {
30
+ cachedInput: !!usage.cachedTokens
31
+ ? {
32
+ credit: cachedInputCredit,
33
+ token: usage.cachedTokens,
34
+ }
35
+ : undefined,
36
+ inputAudio: !!usage.inputAudioTokens
37
+ ? {
38
+ credit: calcCredit(usage.inputAudioTokens, modelCard?.pricing?.audioInput),
39
+ token: usage.inputAudioTokens,
40
+ }
41
+ : undefined,
42
+ inputText: !!usage.inputTokens
43
+ ? {
44
+ credit: calcCredit(
45
+ usage.inputTokens - (usage.inputAudioTokens || 0),
46
+ modelCard?.pricing?.input,
47
+ ),
48
+ token: usage.inputTokens - (usage.inputAudioTokens || 0),
49
+ }
50
+ : undefined,
51
+ outputAudio: !!usage.outputAudioTokens
52
+ ? {
53
+ credit: calcCredit(usage.outputAudioTokens, modelCard?.pricing?.audioOutput),
54
+ id: 'outputAudio',
55
+ token: usage.outputAudioTokens,
56
+ }
57
+ : undefined,
58
+
59
+ outputText: !!usage.outputTokens
60
+ ? {
61
+ credit: calcCredit(
62
+ usage.outputTokens - (usage.reasoningTokens || 0) - (usage.outputAudioTokens || 0),
63
+ modelCard?.pricing?.output,
64
+ ),
65
+ token: usage.outputTokens - (usage.reasoningTokens || 0) - (usage.outputAudioTokens || 0),
66
+ }
67
+ : undefined,
68
+ reasoning: !!usage.reasoningTokens
69
+ ? {
70
+ credit: calcCredit(usage.reasoningTokens, modelCard?.pricing?.output),
71
+ token: usage.reasoningTokens,
72
+ }
73
+ : undefined,
74
+
75
+ totalOutput: !!usage.outputTokens
76
+ ? {
77
+ credit: totalOutput,
78
+ token: usage.outputTokens,
79
+ }
80
+ : undefined,
81
+ totalTokens: !!usage.totalTokens
82
+ ? {
83
+ credit: totalTokens,
84
+ token: usage.totalTokens,
85
+ }
86
+ : undefined,
87
+ uncachedInput: !!usage.inputTokens
88
+ ? {
89
+ credit: uncachedInputCredit,
90
+ token: usage.inputTokens - (usage.cachedTokens || 0),
91
+ }
92
+ : undefined,
93
+ };
94
+ };
@@ -0,0 +1,40 @@
1
+ import { ModelIcon } from '@lobehub/icons';
2
+ import { createStyles } from 'antd-style';
3
+ import { memo } from 'react';
4
+ import { Center, Flexbox } from 'react-layout-kit';
5
+
6
+ import { MessageMetadata } from '@/types/message';
7
+
8
+ import TokenDetail from './UsageDetail';
9
+
10
+ export const useStyles = createStyles(({ token, css, cx }) => ({
11
+ container: cx(css`
12
+ font-size: 12px;
13
+ color: ${token.colorTextQuaternary};
14
+ `),
15
+ }));
16
+
17
+ interface UsageProps {
18
+ metadata: MessageMetadata;
19
+ model: string;
20
+ provider: string;
21
+ }
22
+
23
+ const Usage = memo<UsageProps>(({ model, metadata, provider }) => {
24
+ const { styles } = useStyles();
25
+
26
+ return (
27
+ <Flexbox align={'center'} className={styles.container} horizontal justify={'space-between'}>
28
+ <Center gap={4} horizontal style={{ fontSize: 12 }}>
29
+ <ModelIcon model={model as string} type={'mono'} />
30
+ {model}
31
+ </Center>
32
+
33
+ {!!metadata.totalTokens && (
34
+ <TokenDetail model={model as string} provider={provider} usage={metadata} />
35
+ )}
36
+ </Flexbox>
37
+ );
38
+ });
39
+
40
+ export default Usage;
@@ -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,