feat: add priority demotion controls

This commit is contained in:
2026-05-12 18:43:20 +08:00
parent 3917b84b5d
commit 98abd247d6
22 changed files with 917 additions and 85 deletions
+313 -10
View File
@@ -1,14 +1,19 @@
import { useEffect, useMemo, useState } from 'react';
import { Gauge } from 'lucide-react';
import type { IntegrationPlatform, ModelRateLimitStatus } from '@easyai-ai-gateway/contracts';
import { Badge, Card, CardContent, CardHeader, CardTitle, EmptyState, Table, TableCell, TableHead, TableRow } from '../../components/ui';
import { useEffect, useMemo, useState, type FormEvent } from 'react';
import { Popover as AntPopover } from 'antd';
import { CheckCircle2, Gauge, History, RotateCcw, SlidersHorizontal } from 'lucide-react';
import type { IntegrationPlatform, ModelRateLimitStatus, PlatformDynamicPriorityUpdateRequest, PriorityDemotionRecord } from '@easyai-ai-gateway/contracts';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, EmptyState, FormDialog, Input, Label, Table, TableCell, TableHead, TableRow } from '../../components/ui';
export function RealtimeLoadPanel(props: {
modelRateLimits: ModelRateLimitStatus[];
modelRateLimitsUpdatedAt: number | null;
platforms: IntegrationPlatform[];
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
}) {
const [now, setNow] = useState(() => Date.now());
const [priorityDialog, setPriorityDialog] = useState<PriorityDialogState | null>(null);
const [priorityError, setPriorityError] = useState('');
const [prioritySaving, setPrioritySaving] = useState(false);
const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]);
useEffect(() => {
@@ -16,6 +21,50 @@ export function RealtimeLoadPanel(props: {
return () => window.clearInterval(timer);
}, []);
function openPriorityDialog(status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined) {
setPriorityError('');
setPriorityDialog({
platform,
status,
value: formatPriority(platformEffectivePriority(status, platform)),
});
}
function closePriorityDialog() {
if (prioritySaving) return;
setPriorityDialog(null);
setPriorityError('');
}
async function submitPriorityForm(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!priorityDialog) return;
const dynamicPriority = parsePriorityInput(priorityDialog.value);
if (dynamicPriority === null) {
setPriorityError('请输入大于等于 0 的整数。');
return;
}
await savePriority(priorityDialog.status.platformId, { dynamicPriority });
}
async function resetPriority() {
if (!priorityDialog) return;
await savePriority(priorityDialog.status.platformId, { reset: true });
}
async function savePriority(platformId: string, input: PlatformDynamicPriorityUpdateRequest) {
setPrioritySaving(true);
setPriorityError('');
try {
await props.onSavePlatformDynamicPriority(platformId, input);
setPriorityDialog(null);
} catch (err) {
setPriorityError(err instanceof Error ? err.message : '更新平台动态优先级失败');
} finally {
setPrioritySaving(false);
}
}
return (
<section className="pageStack">
<Card>
@@ -31,14 +80,36 @@ export function RealtimeLoadPanel(props: {
platformMap={platformMap}
statuses={props.modelRateLimits}
updatedAt={props.modelRateLimitsUpdatedAt}
onAdjustPriority={openPriorityDialog}
/>
</CardContent>
</Card>
<PlatformPriorityDialog
dialog={priorityDialog}
error={priorityError}
saving={prioritySaving}
onClose={closePriorityDialog}
onReset={resetPriority}
onSubmit={submitPriorityForm}
onValueChange={(value) => setPriorityDialog((current) => current ? { ...current, value } : current)}
/>
</section>
);
}
function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platformMap: Map<string, IntegrationPlatform>; now: number; updatedAt: number | null }) {
type PriorityDialogState = {
platform: IntegrationPlatform | undefined;
status: ModelRateLimitStatus;
value: string;
};
function RateLimitStatusTable(props: {
statuses: ModelRateLimitStatus[];
platformMap: Map<string, IntegrationPlatform>;
now: number;
updatedAt: number | null;
onAdjustPriority: (status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined) => void;
}) {
if (!props.statuses.length) {
return <EmptyState title="暂无实时负载" description="模型产生请求后会在这里显示实时 RPM、TPM 和并发窗口。" />;
}
@@ -53,6 +124,8 @@ function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platfor
<TableRow className="shTableHeader">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
<TableHead className="platformLimitMetricHead platformLimitNumberHead" title="正在执行 / 并发上限 / 排队任务">
<span></span>
<small> / / </small>
@@ -60,7 +133,6 @@ function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platfor
<TableHead className="platformLimitNumberHead">TPM</TableHead>
<TableHead className="platformLimitNumberHead">RPM</TableHead>
<TableHead className="platformLimitStatusHead"></TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
</TableRow>
{props.statuses.map((status) => {
const platform = props.platformMap.get(status.platformId);
@@ -78,16 +150,17 @@ function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platfor
<small>{status.provider}</small>
</span>
</TableCell>
<TableCell className="platformLimitNumberCell">{concurrencyMetricCell(status)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.tpm, true)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.rpm)}</TableCell>
<TableCell className="platformLimitStatusCell">{modelRuntimeStatusCell(status, props.now)}</TableCell>
<TableCell className="platformLimitNumberCell">{platformPriorityCell(status, platform, props.onAdjustPriority)}</TableCell>
<TableCell className="platformLimitNumberCell">
<span className="rateLoadCell" data-overloaded={status.loadRatio > 0.8 ? 'true' : undefined}>
<strong>{formatPercent(status.loadRatio)}</strong>
<span className="rateLoadTrack"><i style={{ width: `${Math.min(status.loadRatio * 100, 100)}%` }} /></span>
</span>
</TableCell>
<TableCell className="platformLimitNumberCell">{concurrencyMetricCell(status)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.tpm, true)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.rpm)}</TableCell>
<TableCell className="platformLimitStatusCell">{modelRuntimeStatusCell(status, props.now)}</TableCell>
</TableRow>
);
})}
@@ -101,6 +174,176 @@ function platformDisplayName(platform: IntegrationPlatform) {
return platform.internalName?.trim() || platform.name;
}
function platformPriorityCell(
status: ModelRateLimitStatus,
platform: IntegrationPlatform | undefined,
onAdjustPriority: (status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined) => void,
) {
const records = status.recentPriorityDemotions ?? [];
const content = <PriorityDemotionPopover records={records} />;
return (
<span className="platformPriorityCell">
<AntPopover
align={{ offset: [0, 8] }}
content={content}
overlayClassName="priorityDemotionAntPopover"
placement="bottomLeft"
trigger={['hover', 'focus']}
>
<button className="priorityDemotionTrigger" type="button" aria-label={priorityDemotionAriaLabel(status, platform)}>
<span className="rateMetricCell">
<strong>{formatPriority(platformEffectivePriority(status, platform))}</strong>
<small>{platformPrioritySubtitle(status, platform, records.length)}</small>
</span>
</button>
</AntPopover>
<Button className="priorityAdjustButton" type="button" variant="outline" size="xs" onClick={() => onAdjustPriority(status, platform)}>
<SlidersHorizontal size={13} />
</Button>
</span>
);
}
function PlatformPriorityDialog(props: {
dialog: PriorityDialogState | null;
error: string;
saving: boolean;
onClose: () => void;
onReset: () => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
onValueChange: (value: string) => void;
}) {
const dialog = props.dialog;
const platform = dialog?.platform;
const status = dialog?.status;
return (
<FormDialog
bodyClassName="platformPriorityDialogBody"
className="platformPriorityDialog"
closeLabel="关闭"
eyebrow="Runtime Priority"
footer={(
<>
<Button type="button" variant="outline" disabled={props.saving} onClick={props.onClose}>
</Button>
<Button type="button" variant="secondary" disabled={props.saving || !dialog} onClick={props.onReset}>
<RotateCcw size={15} />
</Button>
<Button type="submit" disabled={props.saving || !dialog}>
<CheckCircle2 size={15} />
</Button>
</>
)}
open={Boolean(dialog)}
title="调整平台动态优先级"
onClose={props.onClose}
onSubmit={props.onSubmit}
>
<div className="platformPriorityDialogSummary">
<span>
<strong>{platform ? platformDisplayName(platform) : status?.platformName}</strong>
<small>{status?.provider}</small>
</span>
<span>
<strong>{formatPriority(platformEffectivePriority(status, platform))}</strong>
<small></small>
</span>
</div>
<div className="platformPriorityDialogMetrics">
<span> {formatPriority(platformStaticPriority(status, platform))}</span>
<span> {formatPriority(platformDynamicPriority(status, platform))}</span>
</div>
<Label>
<Input
value={dialog?.value ?? ''}
inputMode="numeric"
min={0}
placeholder="例如 1511"
onChange={(event) => props.onValueChange(event.target.value)}
/>
</Label>
{props.error && <p className="formMessage error">{props.error}</p>}
</FormDialog>
);
}
function PriorityDemotionPopover(props: { records: PriorityDemotionRecord[] }) {
if (!props.records.length) {
return (
<span className="priorityDemotionPopover" role="tooltip">
<span className="priorityDemotionEmpty"></span>
</span>
);
}
return (
<span className="priorityDemotionPopover" role="tooltip">
<span className="priorityDemotionHeader">
<History size={14} />
<strong> 10 </strong>
</span>
{props.records.map((record) => (
<span key={record.id} className="priorityDemotionItem">
<span className="priorityDemotionItemHeader">
<strong>{priorityDemotionReasonText(record)}</strong>
{priorityIsSet(record.dynamicPriority) ? <Badge variant="secondary"> {formatPriority(record.dynamicPriority)}</Badge> : null}
</span>
<small>{priorityDemotionMetaText(record)}</small>
{record.errorMessage && <span className="priorityDemotionError">{record.errorMessage}</span>}
</span>
))}
</span>
);
}
function platformPrioritySubtitle(status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined, demotionCount: number) {
const staticPriority = platformStaticPriority(status, platform);
const dynamicPriority = platformDynamicPriority(status, platform);
const base = priorityIsSet(dynamicPriority) ? `静态 ${formatPriority(staticPriority)} · 动态 ${formatPriority(dynamicPriority)}` : `静态 ${formatPriority(staticPriority)}`;
return demotionCount ? `${base} · 降级 ${demotionCount}` : base;
}
function priorityDemotionAriaLabel(status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined) {
const count = status.recentPriorityDemotions?.length ?? 0;
return `平台优先级 ${formatPriority(platformEffectivePriority(status, platform))},最近 ${count} 条优先级降级记录`;
}
function priorityDemotionReasonText(record: PriorityDemotionRecord) {
const category = record.category ? `错误分类 ${record.category}` : '';
const code = record.errorCode ? `错误 ${record.errorCode}` : '';
const statusCode = record.statusCode ? `状态码 ${record.statusCode}` : '';
return [priorityDemotionReasonLabel(record.reason), statusCode, code, category].filter(Boolean).join(' · ');
}
function priorityDemotionMetaText(record: PriorityDemotionRecord) {
const policy = priorityDemotionPolicyText(record);
const values = [
formatDateTime(record.createdAt),
shortId(record.taskId) ? `任务 ${shortId(record.taskId)}` : '',
policy,
].filter(Boolean);
return values.join(' · ') || '-';
}
function priorityDemotionPolicyText(record: PriorityDemotionRecord) {
const policyPath = [record.policySource || record.policy, record.policyRule].filter(Boolean).join('.');
if (!policyPath) return '';
return record.matchedValue ? `策略 ${policyPath}=${record.matchedValue}` : `策略 ${policyPath}`;
}
function priorityDemotionReasonLabel(reason: string | undefined) {
const labels: Record<string, string> = {
priority_demote_policy: '命中优先级降级规则',
hard_stop_policy: '命中硬拒绝规则',
runner_policy_disabled: '全局调度策略停用',
};
return reason ? labels[reason] ?? reason : '优先级降级';
}
function metricCell(metric: ModelRateLimitStatus['rpm'], includeReserved = false) {
if (!metric.limited) return <span className="rateMetricCell"><strong>{formatLimit(metric.currentValue)} / </strong><small>{includeReserved ? reservedMetricText(metric) : '未配置上限'}</small></span>;
return (
@@ -132,6 +375,19 @@ function formatTimeOfDay(timestamp: number | null) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function formatDateTime(value: string | undefined) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const pad = (item: number) => String(item).padStart(2, '0');
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function shortId(value: string | undefined) {
if (!value) return '';
return value.length > 8 ? value.slice(0, 8) : value;
}
function modelRuntimeStatusCell(status: ModelRateLimitStatus, now: number) {
const modelCooldownMs = cooldownRemainingMs(status.modelCooldownUntil, now);
const platformCooldownMs = cooldownRemainingMs(status.platformCooldownUntil, now);
@@ -184,6 +440,53 @@ function formatLimit(value: number) {
return trimNumber(value);
}
function platformEffectivePriority(status: ModelRateLimitStatus | undefined, platform: IntegrationPlatform | undefined) {
return firstPriority(
status?.platformEffectivePriority,
platform?.effectivePriority,
status?.platformDynamicPriority,
platform?.dynamicPriority,
status?.platformPriority,
platform?.priority,
);
}
function platformStaticPriority(status: ModelRateLimitStatus | undefined, platform: IntegrationPlatform | undefined) {
return firstPriority(status?.platformPriority, platform?.priority);
}
function platformDynamicPriority(status: ModelRateLimitStatus | undefined, platform: IntegrationPlatform | undefined) {
return firstPriority(
status?.platformDynamicPriority,
platform?.dynamicPriority,
status?.platformEffectivePriority,
platform?.effectivePriority,
status?.platformPriority,
platform?.priority,
);
}
function firstPriority(...values: Array<number | undefined>) {
return values.find(priorityIsSet);
}
function priorityIsSet(value: number | undefined): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
function formatPriority(value: number | undefined) {
if (!Number.isFinite(value)) return '-';
return String(Math.trunc(value ?? 0));
}
function parsePriorityInput(value: string) {
const trimmed = value.trim();
if (!/^\d+$/.test(trimmed)) return null;
const parsed = Number(trimmed);
if (!Number.isSafeInteger(parsed)) return null;
return parsed;
}
function trimNumber(value: number) {
return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/\.?0+$/, '');
}
@@ -50,7 +50,6 @@ type RunnerPolicyForm = {
hardStopStatusCodes: string[];
hardStopKeywords: string[];
priorityDemoteEnabled: boolean;
priorityDemoteStep: string;
priorityDemoteCategories: string[];
priorityDemoteCodes: string[];
priorityDemoteStatusCodes: string[];
@@ -406,11 +405,7 @@ function RunnerPolicyEditor(props: {
{activeStrategy === 'priorityDemote' && (
<div className="runtimePolicyRows runnerPolicyDetailRows">
<Toggle checked={props.form.priorityDemoteEnabled} label="启用优先级降级" onChange={(checked) => patch({ priorityDemoteEnabled: checked })} />
<Label>
<Input value={props.form.priorityDemoteStep} inputMode="numeric" onChange={(event) => patch({ priorityDemoteStep: event.target.value })} />
<span className="runtimeFieldHint"> 100</span>
</Label>
<span className="runtimeFieldHint spanTwo"></span>
<KeywordField label="降级分类" value={props.form.priorityDemoteCategories} onChange={(value) => patch({ priorityDemoteCategories: value })} />
<KeywordField label="降级错误码" value={props.form.priorityDemoteCodes} onChange={(value) => patch({ priorityDemoteCodes: value })} />
<KeywordField label="降级状态码" value={props.form.priorityDemoteStatusCodes} onChange={(value) => patch({ priorityDemoteStatusCodes: value })} />
@@ -478,7 +473,6 @@ function runnerPolicyToForm(policy: GatewayRunnerPolicy | null): RunnerPolicyFor
hardStopStatusCodes: tagsFromValue(hardStop.statusCodes ?? []),
hardStopKeywords: tagsFromValue(hardStop.keywords ?? ['invalid_parameter', 'missing required', 'bad request', 'insufficient balance']),
priorityDemoteEnabled: readBool(priorityDemote.enabled, true),
priorityDemoteStep: String(readNumber(priorityDemote.demoteStep, 100)),
priorityDemoteCategories: tagsFromValue(priorityDemote.categories ?? ['network', 'timeout', 'stream_error', 'rate_limit', 'provider_5xx', 'provider_overloaded']),
priorityDemoteCodes: tagsFromValue(priorityDemote.codes ?? ['network', 'timeout', 'stream_read_error', 'rate_limit', 'server_error', 'overloaded']),
priorityDemoteStatusCodes: tagsFromValue(priorityDemote.statusCodes ?? [408, 429, 500, 502, 503, 504]),
@@ -516,7 +510,6 @@ function runnerFormToPayload(form: RunnerPolicyForm): GatewayRunnerPolicyUpsertR
},
priorityDemotePolicy: {
enabled: form.priorityDemoteEnabled,
demoteStep: positiveInt(form.priorityDemoteStep, 100),
categories: cleanTags(form.priorityDemoteCategories),
codes: cleanTags(form.priorityDemoteCodes),
statusCodes: parseNumberTags(form.priorityDemoteStatusCodes),