@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/usage/providers/azureai.mdx +69 -0
- package/docs/usage/providers/azureai.zh-CN.mdx +69 -0
- package/docs/usage/providers/deepseek.mdx +3 -3
- package/docs/usage/providers/deepseek.zh-CN.mdx +5 -4
- package/docs/usage/providers/jina.mdx +51 -0
- package/docs/usage/providers/jina.zh-CN.mdx +51 -0
- package/docs/usage/providers/lmstudio.mdx +75 -0
- package/docs/usage/providers/lmstudio.zh-CN.mdx +75 -0
- package/docs/usage/providers/nvidia.mdx +55 -0
- package/docs/usage/providers/nvidia.zh-CN.mdx +55 -0
- package/docs/usage/providers/ppio.mdx +7 -7
- package/docs/usage/providers/ppio.zh-CN.mdx +6 -6
- package/docs/usage/providers/sambanova.mdx +50 -0
- package/docs/usage/providers/sambanova.zh-CN.mdx +50 -0
- package/docs/usage/providers/tencentcloud.mdx +49 -0
- package/docs/usage/providers/tencentcloud.zh-CN.mdx +49 -0
- package/docs/usage/providers/vertexai.mdx +59 -0
- package/docs/usage/providers/vertexai.zh-CN.mdx +59 -0
- package/docs/usage/providers/vllm.mdx +98 -0
- package/docs/usage/providers/vllm.zh-CN.mdx +98 -0
- package/docs/usage/providers/volcengine.mdx +47 -0
- package/docs/usage/providers/volcengine.zh-CN.mdx +48 -0
- package/locales/ar/chat.json +29 -0
- package/locales/ar/models.json +48 -0
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/chat.json +29 -0
- package/locales/bg-BG/models.json +48 -0
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/chat.json +29 -0
- package/locales/de-DE/models.json +48 -0
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/chat.json +29 -0
- package/locales/en-US/models.json +48 -0
- package/locales/en-US/providers.json +3 -3
- package/locales/es-ES/chat.json +29 -0
- package/locales/es-ES/models.json +48 -0
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/chat.json +29 -0
- package/locales/fa-IR/models.json +48 -0
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/chat.json +29 -0
- package/locales/fr-FR/models.json +48 -0
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/chat.json +29 -0
- package/locales/it-IT/models.json +48 -0
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/chat.json +29 -0
- package/locales/ja-JP/models.json +48 -0
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/chat.json +29 -0
- package/locales/ko-KR/models.json +48 -0
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/chat.json +29 -0
- package/locales/nl-NL/models.json +48 -0
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/chat.json +29 -0
- package/locales/pl-PL/models.json +48 -0
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/chat.json +29 -0
- package/locales/pt-BR/models.json +48 -0
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/chat.json +29 -0
- package/locales/ru-RU/models.json +48 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/chat.json +29 -0
- package/locales/tr-TR/models.json +48 -0
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/chat.json +29 -0
- package/locales/vi-VN/models.json +48 -0
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/chat.json +29 -0
- package/locales/zh-CN/models.json +51 -3
- package/locales/zh-CN/providers.json +3 -4
- package/locales/zh-TW/chat.json +29 -0
- package/locales/zh-TW/models.json +48 -0
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__test__/jina.test.ts +169 -0
- package/packages/web-crawler/src/crawImpl/naive.ts +29 -3
- package/packages/web-crawler/src/utils/errorType.ts +7 -0
- package/scripts/serverLauncher/startServer.js +11 -7
- package/src/config/modelProviders/ppio.ts +1 -1
- package/src/features/Conversation/Extras/Assistant.tsx +12 -20
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +130 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/TokenProgress.tsx +71 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +146 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +94 -0
- package/src/features/Conversation/Extras/Usage/index.tsx +40 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +14 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -0
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +100 -10
- package/src/libs/agent-runtime/utils/streams/openai.ts +30 -4
- package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
- package/src/locales/default/chat.ts +30 -1
- package/src/server/routers/tools/search.ts +1 -1
- package/src/store/aiInfra/slices/aiModel/initialState.ts +3 -1
- package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +5 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +3 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
- package/src/store/chat/slices/message/action.ts +3 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/global/selectors/systemStatus.ts +2 -0
- package/src/types/message/base.ts +18 -0
- package/src/types/message/chat.ts +4 -3
- package/src/utils/fetch/fetchSSE.ts +24 -1
- 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
|
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
|
-
|
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
|
|
@@ -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
|
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
|
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 {
|
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
|
-
{
|
32
|
-
<
|
33
|
-
|
34
|
-
|
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`),
|