@lobehub/chat 1.68.3 → 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 (109) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -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/naive.ts +29 -3
  82. package/packages/web-crawler/src/utils/errorType.ts +7 -0
  83. package/scripts/serverLauncher/startServer.js +11 -7
  84. package/src/config/modelProviders/ppio.ts +1 -1
  85. package/src/features/Conversation/Extras/Assistant.tsx +12 -20
  86. package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +130 -0
  87. package/src/features/Conversation/Extras/Usage/UsageDetail/TokenProgress.tsx +71 -0
  88. package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +146 -0
  89. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +94 -0
  90. package/src/features/Conversation/Extras/Usage/index.tsx +40 -0
  91. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +14 -0
  92. package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -0
  93. package/src/libs/agent-runtime/utils/streams/openai.test.ts +100 -10
  94. package/src/libs/agent-runtime/utils/streams/openai.ts +30 -4
  95. package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
  96. package/src/locales/default/chat.ts +30 -1
  97. package/src/server/routers/tools/search.ts +1 -1
  98. package/src/store/aiInfra/slices/aiModel/initialState.ts +3 -1
  99. package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
  100. package/src/store/aiInfra/slices/aiModel/selectors.ts +5 -0
  101. package/src/store/aiInfra/slices/aiProvider/action.ts +3 -1
  102. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
  103. package/src/store/chat/slices/message/action.ts +3 -0
  104. package/src/store/global/initialState.ts +1 -0
  105. package/src/store/global/selectors/systemStatus.ts +2 -0
  106. package/src/types/message/base.ts +18 -0
  107. package/src/types/message/chat.ts +4 -3
  108. package/src/utils/fetch/fetchSSE.ts +24 -1
  109. package/src/utils/format.ts +3 -1
@@ -1,5 +1,5 @@
1
1
  import { CrawlImpl, CrawlSuccessResult } from '../type';
2
- import { NetworkConnectionError, PageNotFoundError } from '../utils/errorType';
2
+ import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../utils/errorType';
3
3
  import { htmlToMarkdown } from '../utils/htmlToMarkdown';
4
4
 
5
5
  const mixinHeaders = {
@@ -31,15 +31,41 @@ const mixinHeaders = {
31
31
  'sec-fetch-user': '?1',
32
32
  };
33
33
 
34
+ const TIMEOUT_CONTROL = 10_000;
35
+
36
+ const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> => {
37
+ const controller = new AbortController();
38
+ const timeoutPromise = new Promise<T>((_, reject) => {
39
+ setTimeout(() => {
40
+ controller.abort();
41
+ reject(new TimeoutError(`Request timeout after ${ms}ms`));
42
+ }, ms);
43
+ });
44
+
45
+ return Promise.race([promise, timeoutPromise]);
46
+ };
47
+
34
48
  export const naive: CrawlImpl = async (url, { filterOptions }) => {
35
49
  let res: Response;
36
50
 
37
51
  try {
38
- res = await fetch(url, { headers: mixinHeaders });
52
+ res = await withTimeout(
53
+ fetch(url, {
54
+ headers: mixinHeaders,
55
+ signal: new AbortController().signal,
56
+ }),
57
+ TIMEOUT_CONTROL,
58
+ );
39
59
  } catch (e) {
40
- if ((e as Error).message === 'fetch failed') {
60
+ const error = e as Error;
61
+ if (error.message === 'fetch failed') {
41
62
  throw new NetworkConnectionError();
42
63
  }
64
+
65
+ if (error instanceof TimeoutError) {
66
+ throw error;
67
+ }
68
+
43
69
  throw e;
44
70
  }
45
71
 
@@ -10,3 +10,10 @@ export class NetworkConnectionError extends Error {
10
10
  this.name = 'NetworkConnectionError';
11
11
  }
12
12
  }
13
+
14
+ export class TimeoutError extends Error {
15
+ constructor(message: string) {
16
+ super(message);
17
+ this.name = 'TimeoutError';
18
+ }
19
+ }
@@ -74,20 +74,24 @@ const runProxyChainsConfGenerator = async (url) => {
74
74
 
75
75
  let ip = isValidIP(host, 4) ? host : await resolveHostIP(host, 4);
76
76
 
77
- const configContent = `
78
- localnet 127.0.0.0/255.0.0.0
79
- localnet 10.0.0.0/255.0.0.0
80
- localnet 172.16.0.0/255.240.0.0
81
- localnet 192.168.0.0/255.255.0.0
82
- localnet ::1/128
77
+ const proxyDNSConfig = process.env.ENABLE_PROXY_DNS === '1' ? `
83
78
  proxy_dns
84
79
  remote_dns_subnet 224
80
+ `.trim() : '';
81
+
82
+ const configContent = `
83
+ localnet 127.0.0.0/8
84
+ localnet 10.0.0.0/8
85
+ localnet 172.16.0.0/12
86
+ localnet 192.168.0.0/16
87
+ localnet ::/127
88
+ ${proxyDNSConfig}
85
89
  strict_chain
86
90
  tcp_connect_time_out 8000
87
91
  tcp_read_time_out 15000
88
92
  [ProxyList]
89
93
  ${protocol} ${ip} ${port} ${user} ${pass}
90
- `.trim();
94
+ `.replace(/\n{2,}/g, '\n').trim();
91
95
 
92
96
  await fs.writeFile(PROXYCHAINS_CONF_PATH, configContent);
93
97
  console.log(`✅ ProxyChains: All outgoing traffic routed via ${url}.`);
@@ -243,7 +243,7 @@ const PPIO: ModelProviderCard = {
243
243
  sdkType: 'openai',
244
244
  showModelFetcher: true,
245
245
  },
246
- url: 'https://ppinfra.com/?utm_source=github_lobe-chat&utm_medium=github_readme&utm_campaign=link',
246
+ url: 'https://ppinfra.com/user/register?invited_by=RQIMOC',
247
247
  };
248
248
 
249
249
  export default PPIO;
@@ -1,9 +1,7 @@
1
- import { ModelTag } from '@lobehub/icons';
2
1
  import { memo } from 'react';
3
2
  import { Flexbox } from 'react-layout-kit';
4
3
 
5
- import { useAgentStore } from '@/store/agent';
6
- import { agentSelectors } from '@/store/agent/slices/chat';
4
+ import { LOADING_FLAT } from '@/const/message';
7
5
  import { useChatStore } from '@/store/chat';
8
6
  import { chatSelectors } from '@/store/chat/selectors';
9
7
  import { ChatMessage } from '@/types/message';
@@ -12,34 +10,28 @@ import { RenderMessageExtra } from '../types';
12
10
  import ExtraContainer from './ExtraContainer';
13
11
  import TTS from './TTS';
14
12
  import Translate from './Translate';
13
+ import Usage from './Usage';
15
14
 
16
15
  export const AssistantMessageExtra: RenderMessageExtra = memo<ChatMessage>(
17
- ({ extra, id, content }) => {
18
- const model = useAgentStore(agentSelectors.currentAgentModel);
16
+ ({ extra, id, content, metadata, tools }) => {
19
17
  const loading = useChatStore(chatSelectors.isMessageGenerating(id));
20
18
 
21
- const showModelTag = extra?.fromModel && model !== extra?.fromModel;
22
- const showTranslate = !!extra?.translate;
23
- const showTTS = !!extra?.tts;
24
-
25
- const showExtra = showModelTag || showTranslate || showTTS;
26
-
27
- if (!showExtra) return;
28
-
29
19
  return (
30
- <Flexbox gap={8} style={{ marginTop: 8 }}>
31
- {showModelTag && (
32
- <div>
33
- <ModelTag model={extra?.fromModel as string} />
34
- </div>
20
+ <Flexbox gap={8} style={{ marginTop: !!tools?.length ? 8 : 4 }}>
21
+ {content !== LOADING_FLAT && extra?.fromModel && (
22
+ <Usage
23
+ metadata={metadata || {}}
24
+ model={extra?.fromModel}
25
+ provider={extra.fromProvider!}
26
+ />
35
27
  )}
36
28
  <>
37
- {extra?.tts && (
29
+ {!!extra?.tts && (
38
30
  <ExtraContainer>
39
31
  <TTS content={content} id={id} loading={loading} {...extra?.tts} />
40
32
  </ExtraContainer>
41
33
  )}
42
- {extra?.translate && (
34
+ {!!extra?.translate && (
43
35
  <ExtraContainer>
44
36
  <Translate id={id} loading={loading} {...extra?.translate} />
45
37
  </ExtraContainer>
@@ -0,0 +1,130 @@
1
+ import { ModelIcon } from '@lobehub/icons';
2
+ import { Icon, Tooltip } from '@lobehub/ui';
3
+ import { Segmented } from 'antd';
4
+ import { createStyles } from 'antd-style';
5
+ import { ArrowDownToDot, ArrowUpFromDot, CircleFadingArrowUp } from 'lucide-react';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { useGlobalStore } from '@/store/global';
11
+ import { systemStatusSelectors } from '@/store/global/selectors';
12
+ import { LobeDefaultAiModelListItem } from '@/types/aiModel';
13
+ import { ModelPriceCurrency } from '@/types/llm';
14
+ import { formatPriceByCurrency } from '@/utils/format';
15
+
16
+ export const useStyles = createStyles(({ css, token }) => {
17
+ return {
18
+ container: css`
19
+ font-size: 12px;
20
+ `,
21
+ desc: css`
22
+ line-height: 12px;
23
+ color: ${token.colorTextDescription};
24
+ `,
25
+ pricing: css`
26
+ font-size: 12px;
27
+ color: ${token.colorTextSecondary};
28
+ `,
29
+ };
30
+ });
31
+
32
+ interface ModelCardProps extends LobeDefaultAiModelListItem {
33
+ provider: string;
34
+ }
35
+
36
+ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName }) => {
37
+ const { t } = useTranslation('chat');
38
+ const { styles } = useStyles();
39
+
40
+ const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!pricing;
41
+ const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
42
+
43
+ const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
44
+ const cachedInputPrice = formatPriceByCurrency(
45
+ pricing?.cachedInput,
46
+ pricing?.currency as ModelPriceCurrency,
47
+ );
48
+ const outputPrice = formatPriceByCurrency(
49
+ pricing?.output,
50
+ pricing?.currency as ModelPriceCurrency,
51
+ );
52
+ return (
53
+ <>
54
+ <Flexbox
55
+ align={'center'}
56
+ className={styles.container}
57
+ flex={1}
58
+ gap={40}
59
+ horizontal
60
+ justify={'space-between'}
61
+ >
62
+ <Flexbox align={'center'} gap={8} horizontal>
63
+ <ModelIcon model={id} size={22} />
64
+ <Flexbox flex={1} gap={2} style={{ minWidth: 0 }}>
65
+ <Flexbox align={'center'} gap={8} horizontal style={{ lineHeight: '12px' }}>
66
+ {displayName || id}
67
+ </Flexbox>
68
+ <span className={styles.desc}>{provider}</span>
69
+ </Flexbox>
70
+ </Flexbox>
71
+ {!!pricing && (
72
+ <Flexbox>
73
+ <Segmented
74
+ onChange={(value) => {
75
+ updateSystemStatus({ isShowCredit: value === 'credit' });
76
+ }}
77
+ options={[
78
+ { label: 'Token', value: 'token' },
79
+ {
80
+ label: (
81
+ <Tooltip title={t('messages.modelCard.creditTooltip')}>
82
+ {t('messages.modelCard.credit')}
83
+ </Tooltip>
84
+ ),
85
+ value: 'credit',
86
+ },
87
+ ]}
88
+ size={'small'}
89
+ value={isShowCredit ? 'credit' : 'token'}
90
+ />
91
+ </Flexbox>
92
+ )}
93
+ </Flexbox>
94
+ {isShowCredit && (
95
+ <Flexbox horizontal justify={'space-between'}>
96
+ <div />
97
+ <Flexbox align={'center'} className={styles.pricing} gap={8} horizontal>
98
+ {t('messages.modelCard.creditPricing')}:
99
+ {pricing?.cachedInput && (
100
+ <Tooltip
101
+ title={t('messages.modelCard.pricing.inputCachedTokens', {
102
+ amount: cachedInputPrice,
103
+ })}
104
+ >
105
+ <Flexbox gap={2} horizontal>
106
+ <Icon icon={CircleFadingArrowUp} />
107
+ {cachedInputPrice}
108
+ </Flexbox>
109
+ </Tooltip>
110
+ )}
111
+ <Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
112
+ <Flexbox gap={2} horizontal>
113
+ <Icon icon={ArrowUpFromDot} />
114
+ {inputPrice}
115
+ </Flexbox>
116
+ </Tooltip>
117
+ <Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
118
+ <Flexbox gap={2} horizontal>
119
+ <Icon icon={ArrowDownToDot} />
120
+ {outputPrice}
121
+ </Flexbox>
122
+ </Tooltip>
123
+ </Flexbox>
124
+ </Flexbox>
125
+ )}
126
+ </>
127
+ );
128
+ });
129
+
130
+ export default ModelCard;
@@ -0,0 +1,71 @@
1
+ import { useTheme } from 'antd-style';
2
+ import numeral from 'numeral';
3
+ import { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+
7
+ export interface TokenProgressItem {
8
+ color: string;
9
+ id: string;
10
+ title: string;
11
+ value: number;
12
+ }
13
+
14
+ interface TokenProgressProps {
15
+ data: TokenProgressItem[];
16
+ showIcon?: boolean;
17
+ }
18
+
19
+ const format = (number: number) => numeral(number).format('0,0');
20
+
21
+ const TokenProgress = memo<TokenProgressProps>(({ data, showIcon }) => {
22
+ const theme = useTheme();
23
+ const total = data.reduce((acc, item) => acc + item.value, 0);
24
+
25
+ return (
26
+ <Flexbox gap={8} style={{ position: 'relative' }} width={'100%'}>
27
+ <Flexbox
28
+ height={6}
29
+ horizontal
30
+ style={{
31
+ background: total === 0 ? theme.colorFill : undefined,
32
+ borderRadius: 3,
33
+ overflow: 'hidden',
34
+ position: 'relative',
35
+ }}
36
+ width={'100%'}
37
+ >
38
+ {data.map((item) => (
39
+ <Flexbox
40
+ height={'100%'}
41
+ key={item.id}
42
+ style={{ background: item.color, flex: item.value }}
43
+ />
44
+ ))}
45
+ </Flexbox>
46
+ <Flexbox>
47
+ {data.map((item) => (
48
+ <Flexbox align={'center'} gap={4} horizontal justify={'space-between'} key={item.id}>
49
+ <Flexbox align={'center'} gap={4} horizontal>
50
+ {showIcon && (
51
+ <div
52
+ style={{
53
+ background: item.color,
54
+ borderRadius: '50%',
55
+ flex: 'none',
56
+ height: 6,
57
+ width: 6,
58
+ }}
59
+ />
60
+ )}
61
+ <div style={{ color: theme.colorTextSecondary }}>{item.title}</div>
62
+ </Flexbox>
63
+ <div style={{ fontWeight: 500 }}>{format(item.value)}</div>
64
+ </Flexbox>
65
+ ))}
66
+ </Flexbox>
67
+ </Flexbox>
68
+ );
69
+ });
70
+
71
+ export default TokenProgress;
@@ -0,0 +1,146 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Divider, Popover } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import { BadgeCent, CoinsIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Center, Flexbox } from 'react-layout-kit';
8
+
9
+ import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
10
+ import { useGlobalStore } from '@/store/global';
11
+ import { systemStatusSelectors } from '@/store/global/selectors';
12
+ import { ModelTokensUsage } from '@/types/message';
13
+ import { formatNumber } from '@/utils/format';
14
+
15
+ import ModelCard from './ModelCard';
16
+ import TokenProgress, { TokenProgressItem } from './TokenProgress';
17
+ import { getDetailsToken } from './tokens';
18
+
19
+ interface TokenDetailProps {
20
+ model: string;
21
+ provider: string;
22
+ usage: ModelTokensUsage;
23
+ }
24
+
25
+ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
26
+ const { t } = useTranslation('chat');
27
+ const theme = useTheme();
28
+
29
+ const modelCard = useAiInfraStore(aiModelSelectors.getModelCard(model, provider));
30
+ const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!modelCard?.pricing;
31
+
32
+ const detailTokens = getDetailsToken(usage, modelCard);
33
+ const inputDetails = [
34
+ !!detailTokens.inputAudio && {
35
+ color: theme.cyan9,
36
+ id: 'reasoning',
37
+ title: t('messages.tokenDetails.inputAudio'),
38
+ value: isShowCredit ? detailTokens.inputAudio.credit : detailTokens.inputAudio.token,
39
+ },
40
+ !!detailTokens.inputText && {
41
+ color: theme.green,
42
+ id: 'inputText',
43
+ title: t('messages.tokenDetails.inputText'),
44
+ value: isShowCredit ? detailTokens.inputText.credit : detailTokens.inputText.token,
45
+ },
46
+ ].filter(Boolean) as TokenProgressItem[];
47
+
48
+ const outputDetails = [
49
+ !!detailTokens.reasoning && {
50
+ color: theme.pink,
51
+ id: 'reasoning',
52
+ title: t('messages.tokenDetails.reasoning'),
53
+ value: isShowCredit ? detailTokens.reasoning.credit : detailTokens.reasoning.token,
54
+ },
55
+ !!detailTokens.outputAudio && {
56
+ color: theme.cyan9,
57
+ id: 'outputAudio',
58
+ title: t('messages.tokenDetails.outputAudio'),
59
+ value: isShowCredit ? detailTokens.outputAudio.credit : detailTokens.outputAudio.token,
60
+ },
61
+ !!detailTokens.outputText && {
62
+ color: theme.green,
63
+ id: 'outputText',
64
+ title: t('messages.tokenDetails.outputText'),
65
+ value: isShowCredit ? detailTokens.outputText.credit : detailTokens.outputText.token,
66
+ },
67
+ ].filter(Boolean) as TokenProgressItem[];
68
+
69
+ const totalDetail = [
70
+ !!detailTokens.cachedInput && {
71
+ color: theme.orange,
72
+ id: 'cachedInput',
73
+ title: t('messages.tokenDetails.inputCached'),
74
+ value: isShowCredit ? detailTokens.cachedInput.credit : detailTokens.cachedInput.token,
75
+ },
76
+ !!detailTokens.uncachedInput && {
77
+ color: theme.colorFill,
78
+
79
+ id: 'uncachedInput',
80
+ title: t('messages.tokenDetails.inputUncached'),
81
+ value: isShowCredit ? detailTokens.uncachedInput.credit : detailTokens.uncachedInput.token,
82
+ },
83
+ !!detailTokens.totalOutput && {
84
+ color: theme.colorSuccess,
85
+ id: 'output',
86
+ title: t('messages.tokenDetails.output'),
87
+ value: isShowCredit ? detailTokens.totalOutput.credit : detailTokens.totalOutput.token,
88
+ },
89
+ ].filter(Boolean) as TokenProgressItem[];
90
+
91
+ const displayTotal =
92
+ isShowCredit && !!detailTokens.totalTokens
93
+ ? formatNumber(detailTokens.totalTokens.credit)
94
+ : formatNumber(usage.totalTokens);
95
+
96
+ return (
97
+ <Popover
98
+ arrow={false}
99
+ content={
100
+ <Flexbox gap={20} style={{ minWidth: 200 }}>
101
+ {modelCard && <ModelCard {...modelCard} provider={provider} />}
102
+ {inputDetails.length > 1 && (
103
+ <>
104
+ <Flexbox align={'center'} gap={4} horizontal justify={'space-between'} width={'100%'}>
105
+ <div style={{ color: theme.colorTextDescription }}>
106
+ {t('messages.tokenDetails.inputTitle')}
107
+ </div>
108
+ </Flexbox>
109
+ <TokenProgress data={inputDetails} showIcon />
110
+ </>
111
+ )}
112
+ {outputDetails.length > 1 && (
113
+ <>
114
+ <Flexbox align={'center'} gap={4} horizontal justify={'space-between'} width={'100%'}>
115
+ <div style={{ color: theme.colorTextDescription }}>
116
+ {t('messages.tokenDetails.outputTitle')}
117
+ </div>
118
+ </Flexbox>
119
+ <TokenProgress data={outputDetails} showIcon />
120
+ </>
121
+ )}
122
+
123
+ <Flexbox>
124
+ <TokenProgress data={totalDetail} showIcon />
125
+ <Divider style={{ marginBlock: 8 }} />
126
+ <Flexbox align={'center'} gap={4} horizontal justify={'space-between'}>
127
+ <div style={{ color: theme.colorTextSecondary }}>
128
+ {t('messages.tokenDetails.total')}
129
+ </div>
130
+ <div style={{ fontWeight: 500 }}>{displayTotal}</div>
131
+ </Flexbox>
132
+ </Flexbox>
133
+ </Flexbox>
134
+ }
135
+ placement={'top'}
136
+ trigger={['hover', 'click']}
137
+ >
138
+ <Center gap={2} horizontal style={{ cursor: 'default' }}>
139
+ <Icon icon={isShowCredit ? BadgeCent : CoinsIcon} />
140
+ {displayTotal}
141
+ </Center>
142
+ </Popover>
143
+ );
144
+ });
145
+
146
+ export default TokenDetail;
@@ -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`),