feat: channel affinity (#2669)
* feat: channel affinity * feat: channel affinity -> model setting * fix: channel affinity * feat: channel affinity op * feat: channel_type setting * feat: clean * feat: cache supports both memory and Redis. * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: Optimise codex usage ui/ux * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: If the affinitized channel fails and a retry succeeds on another channel, update the affinity to the successful channel
This commit is contained in:
@@ -17,8 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal, Button, Progress, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Modal, Button, Progress, Tag, Typography, Spin } from '@douyinfe/semi-ui';
|
||||
import { API, showError } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -101,7 +102,7 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const data = payload?.data ?? null;
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
@@ -123,61 +124,159 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const rawText =
|
||||
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
||||
|
||||
Modal.info({
|
||||
title: (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{tt('Codex 用量')}</span>
|
||||
{statusTag}
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
{statusTag}
|
||||
<Button size='small' type='tertiary' theme='borderless' onClick={onRefresh}>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const [loading, setLoading] = useState(!initialPayload);
|
||||
const [payload, setPayload] = useState(initialPayload ?? null);
|
||||
const hasShownErrorRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const recordId = record?.id;
|
||||
|
||||
const fetchUsage = useCallback(async () => {
|
||||
if (!recordId) {
|
||||
if (mountedRef.current) setPayload(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mountedRef.current) setLoading(true);
|
||||
try {
|
||||
const res = await API.get(`/api/channel/${recordId}/codex/usage`, {
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
if (!mountedRef.current) return;
|
||||
setPayload(res?.data ?? null);
|
||||
if (!res?.data?.success && !hasShownErrorRef.current) {
|
||||
hasShownErrorRef.current = true;
|
||||
showError(tt('获取用量失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mountedRef.current) return;
|
||||
if (!hasShownErrorRef.current) {
|
||||
hasShownErrorRef.current = true;
|
||||
showError(tt('获取用量失败'));
|
||||
}
|
||||
setPayload({ success: false, message: String(error) });
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, [recordId, tt]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPayload) return;
|
||||
fetchUsage().catch(() => {});
|
||||
}, [fetchUsage, initialPayload]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-10'>
|
||||
<Spin spinning={true} size='large' tip={tt('加载中...')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<Text type='danger'>{tt('获取用量失败')}</Text>
|
||||
<div className='flex justify-end'>
|
||||
<Button size='small' type='primary' theme='outline' onClick={fetchUsage}>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodexUsageView
|
||||
t={tt}
|
||||
record={record}
|
||||
payload={payload}
|
||||
onCopy={onCopy}
|
||||
onRefresh={fetchUsage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
|
||||
Modal.info({
|
||||
title: tt('Codex 用量'),
|
||||
centered: true,
|
||||
width: 900,
|
||||
style: { maxWidth: '95vw' },
|
||||
content: (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<CodexUsageLoader
|
||||
t={tt}
|
||||
record={record}
|
||||
initialPayload={payload}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
),
|
||||
footer: (
|
||||
<div className='flex justify-end gap-2'>
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
renderClaudeModelPrice,
|
||||
renderModelPrice,
|
||||
} from '../../../helpers';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { IconHelpCircle, IconStarStroked } from '@douyinfe/semi-icons';
|
||||
import { Route } from 'lucide-react';
|
||||
|
||||
const colors = [
|
||||
@@ -498,6 +498,7 @@ export const getLogsColumns = ({
|
||||
return <></>;
|
||||
}
|
||||
let content = t('渠道') + `:${record.channel}`;
|
||||
let affinity = null;
|
||||
if (record.other !== '') {
|
||||
let other = JSON.parse(record.other);
|
||||
if (other === null) {
|
||||
@@ -513,9 +514,55 @@ export const getLogsColumns = ({
|
||||
let useChannelStr = useChannel.join('->');
|
||||
content = t('渠道') + `:${useChannelStr}`;
|
||||
}
|
||||
if (other.admin_info.channel_affinity) {
|
||||
affinity = other.admin_info.channel_affinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAdminUser ? <div>{content}</div> : <></>;
|
||||
return isAdminUser ? (
|
||||
<Space>
|
||||
<div>{content}</div>
|
||||
{affinity ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<div style={{ lineHeight: 1.6 }}>
|
||||
<Typography.Text strong>{t('渠道亲和性')}</Typography.Text>
|
||||
<div>
|
||||
<Typography.Text type='secondary'>
|
||||
{t('规则')}:{affinity.rule_name || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type='secondary'>
|
||||
{t('分组')}:{affinity.selected_group || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type='secondary'>
|
||||
{t('Key')}:
|
||||
{(affinity.key_source || '-') +
|
||||
':' +
|
||||
(affinity.key_path || affinity.key_key || '-') +
|
||||
(affinity.key_fp ? `#${affinity.key_fp}` : '')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Tag className='channel-affinity-tag' color='cyan' shape='circle'>
|
||||
<span className='channel-affinity-tag-content'>
|
||||
<IconStarStroked style={{ fontSize: 13 }} />
|
||||
{t('优选')}
|
||||
</span>
|
||||
</Tag>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -552,9 +599,13 @@ export const getLogsColumns = ({
|
||||
other.cache_creation_tokens || 0,
|
||||
other.cache_creation_ratio || 1.0,
|
||||
other.cache_creation_tokens_5m || 0,
|
||||
other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0,
|
||||
other.cache_creation_ratio_5m ||
|
||||
other.cache_creation_ratio ||
|
||||
1.0,
|
||||
other.cache_creation_tokens_1h || 0,
|
||||
other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0,
|
||||
other.cache_creation_ratio_1h ||
|
||||
other.cache_creation_ratio ||
|
||||
1.0,
|
||||
false,
|
||||
1.0,
|
||||
other?.is_system_prompt_overwritten,
|
||||
|
||||
Reference in New Issue
Block a user