| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useMemo, useRef, useState } from 'react'; |
| import { Modal, Descriptions, Spin, Typography } from '@douyinfe/semi-ui'; |
| import { API, showError, timestamp2string } from '../../../../helpers'; |
|
|
| const { Text } = Typography; |
|
|
| function formatRate(hit, total) { |
| if (!total || total <= 0) return '-'; |
| const r = (Number(hit || 0) / Number(total || 0)) * 100; |
| if (!Number.isFinite(r)) return '-'; |
| return `${r.toFixed(2)}%`; |
| } |
|
|
| function formatTokenRate(n, d) { |
| const nn = Number(n || 0); |
| const dd = Number(d || 0); |
| if (!dd || dd <= 0) return '-'; |
| const r = (nn / dd) * 100; |
| if (!Number.isFinite(r)) return '-'; |
| return `${r.toFixed(2)}%`; |
| } |
|
|
| function formatCachedTokenRate(cachedTokens, promptTokens, mode) { |
| if (mode === 'cached_over_prompt_plus_cached') { |
| const denominator = Number(promptTokens || 0) + Number(cachedTokens || 0); |
| return formatTokenRate(cachedTokens, denominator); |
| } |
| if (mode === 'cached_over_prompt') { |
| return formatTokenRate(cachedTokens, promptTokens); |
| } |
| return '-'; |
| } |
|
|
| function hasTextValue(value) { |
| return typeof value === 'string' && value.trim() !== ''; |
| } |
|
|
| const ChannelAffinityUsageCacheModal = ({ |
| t, |
| showChannelAffinityUsageCacheModal, |
| setShowChannelAffinityUsageCacheModal, |
| channelAffinityUsageCacheTarget, |
| }) => { |
| const [loading, setLoading] = useState(false); |
| const [stats, setStats] = useState(null); |
| const requestSeqRef = useRef(0); |
|
|
| const params = useMemo(() => { |
| const x = channelAffinityUsageCacheTarget || {}; |
| return { |
| rule_name: (x.rule_name || '').trim(), |
| using_group: (x.using_group || '').trim(), |
| key_hint: (x.key_hint || '').trim(), |
| key_fp: (x.key_fp || '').trim(), |
| }; |
| }, [channelAffinityUsageCacheTarget]); |
|
|
| useEffect(() => { |
| if (!showChannelAffinityUsageCacheModal) { |
| requestSeqRef.current += 1; |
| setLoading(false); |
| setStats(null); |
| return; |
| } |
| if (!params.rule_name || !params.key_fp) { |
| setLoading(false); |
| setStats(null); |
| return; |
| } |
|
|
| const reqSeq = (requestSeqRef.current += 1); |
| setStats(null); |
| setLoading(true); |
| (async () => { |
| try { |
| const res = await API.get('/api/log/channel_affinity_usage_cache', { |
| params, |
| disableDuplicate: true, |
| }); |
| if (reqSeq !== requestSeqRef.current) return; |
| const { success, message, data } = res.data || {}; |
| if (!success) { |
| setStats(null); |
| showError(t(message || '请求失败')); |
| return; |
| } |
| setStats(data || {}); |
| } catch (e) { |
| if (reqSeq !== requestSeqRef.current) return; |
| setStats(null); |
| showError(t('请求失败')); |
| } finally { |
| if (reqSeq !== requestSeqRef.current) return; |
| setLoading(false); |
| } |
| })(); |
| }, [ |
| showChannelAffinityUsageCacheModal, |
| params.rule_name, |
| params.using_group, |
| params.key_hint, |
| params.key_fp, |
| t, |
| ]); |
|
|
| const { rows, supportsTokenStats } = useMemo(() => { |
| const s = stats || {}; |
| const hit = Number(s.hit || 0); |
| const total = Number(s.total || 0); |
| const windowSeconds = Number(s.window_seconds || 0); |
| const lastSeenAt = Number(s.last_seen_at || 0); |
| const promptTokens = Number(s.prompt_tokens || 0); |
| const completionTokens = Number(s.completion_tokens || 0); |
| const totalTokens = Number(s.total_tokens || 0); |
| const cachedTokens = Number(s.cached_tokens || 0); |
| const promptCacheHitTokens = Number(s.prompt_cache_hit_tokens || 0); |
| const cachedTokenRateMode = String(s.cached_token_rate_mode || '').trim(); |
| const supportsTokenStats = |
| cachedTokenRateMode === 'cached_over_prompt' || |
| cachedTokenRateMode === 'cached_over_prompt_plus_cached' || |
| cachedTokenRateMode === 'mixed'; |
|
|
| const data = []; |
| const ruleName = String(s.rule_name || params.rule_name || '').trim(); |
| const usingGroup = String(s.using_group || params.using_group || '').trim(); |
| const keyHint = String(params.key_hint || '').trim(); |
| const keyFp = String(s.key_fp || params.key_fp || '').trim(); |
|
|
| if (hasTextValue(ruleName)) { |
| data.push({ key: t('规则'), value: ruleName }); |
| } |
| if (hasTextValue(usingGroup)) { |
| data.push({ key: t('分组'), value: usingGroup }); |
| } |
| if (hasTextValue(keyHint)) { |
| data.push({ key: t('Key 摘要'), value: keyHint }); |
| } |
| if (hasTextValue(keyFp)) { |
| data.push({ key: t('Key 指纹'), value: keyFp }); |
| } |
| if (windowSeconds > 0) { |
| data.push({ key: t('TTL(秒)'), value: windowSeconds }); |
| } |
| if (total > 0) { |
| data.push({ key: t('命中率'), value: `${hit}/${total} (${formatRate(hit, total)})` }); |
| } |
| if (lastSeenAt > 0) { |
| data.push({ key: t('最近一次'), value: timestamp2string(lastSeenAt) }); |
| } |
|
|
| if (supportsTokenStats) { |
| if (promptTokens > 0) { |
| data.push({ key: t('Prompt tokens'), value: promptTokens }); |
| } |
| if (promptTokens > 0 || cachedTokens > 0) { |
| data.push({ |
| key: t('Cached tokens'), |
| value: `${cachedTokens} (${formatCachedTokenRate(cachedTokens, promptTokens, cachedTokenRateMode)})`, |
| }); |
| } |
| if (promptCacheHitTokens > 0) { |
| data.push({ key: t('Prompt cache hit tokens'), value: promptCacheHitTokens }); |
| } |
| if (completionTokens > 0) { |
| data.push({ key: t('Completion tokens'), value: completionTokens }); |
| } |
| if (totalTokens > 0) { |
| data.push({ key: t('Total tokens'), value: totalTokens }); |
| } |
| } |
|
|
| return { rows: data, supportsTokenStats }; |
| }, [stats, params, t]); |
|
|
| return ( |
| <Modal |
| title={t('渠道亲和性:上游缓存命中')} |
| visible={showChannelAffinityUsageCacheModal} |
| onCancel={() => setShowChannelAffinityUsageCacheModal(false)} |
| footer={null} |
| centered |
| closable |
| maskClosable |
| width={640} |
| > |
| <div style={{ padding: 16 }}> |
| <div style={{ marginBottom: 12 }}> |
| <Text type='tertiary' size='small'> |
| {t( |
| '命中判定:usage 中存在 cached tokens(例如 cached_tokens/prompt_cache_hit_tokens)即视为命中。', |
| )} |
| {' '} |
| {t( |
| 'Cached tokens 占比口径由后端返回:Claude 语义按 cached/(prompt+cached),其余按 cached/prompt。', |
| )} |
| {' '} |
| {t('当前仅 OpenAI / Claude 语义支持缓存 token 统计,其他通道将隐藏 token 相关字段。')} |
| {stats && !supportsTokenStats ? ( |
| <> |
| {' '} |
| {t('该记录不包含可用的 token 统计口径。')} |
| </> |
| ) : null} |
| </Text> |
| </div> |
| <Spin spinning={loading} tip={t('加载中...')}> |
| {stats && rows.length > 0 ? ( |
| <Descriptions data={rows} /> |
| ) : ( |
| <div style={{ padding: '24px 0' }}> |
| <Text type='tertiary' size='small'> |
| {loading ? t('加载中...') : t('暂无可展示数据')} |
| </Text> |
| </div> |
| )} |
| </Spin> |
| </div> |
| </Modal> |
| ); |
| }; |
|
|
| export default ChannelAffinityUsageCacheModal; |
|
|