Decouple stream cancellation and clarify failover rules
This commit is contained in:
@@ -58,6 +58,39 @@ type RunnerPolicyForm = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
const failoverActionDefinitions = [
|
||||
{
|
||||
value: 'next',
|
||||
title: '仅轮转',
|
||||
description: '这些错误分类只尝试下一个平台,不修改当前平台状态。',
|
||||
},
|
||||
{
|
||||
value: 'disable_and_next',
|
||||
title: '禁用后轮转',
|
||||
description: '这些错误分类会先禁用当前平台,再尝试下一个平台。',
|
||||
},
|
||||
{
|
||||
value: 'cooldown_and_next',
|
||||
title: '冷却后轮转',
|
||||
description: '这些错误分类会先让当前平台模型进入冷却,再尝试下一个平台。',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const failoverCategoryOptions = [
|
||||
'network',
|
||||
'timeout',
|
||||
'stream_error',
|
||||
'rate_limit',
|
||||
'provider_5xx',
|
||||
'provider_overloaded',
|
||||
'auth_error',
|
||||
'request_error',
|
||||
'unsupported_model',
|
||||
'user_permission',
|
||||
'insufficient_balance',
|
||||
'client_error',
|
||||
].map((item) => ({ label: item, value: item }));
|
||||
|
||||
export function RuntimePoliciesPanel(props: {
|
||||
message: string;
|
||||
runnerPolicy: GatewayRunnerPolicy | null;
|
||||
@@ -380,14 +413,22 @@ function RunnerPolicyEditor(props: {
|
||||
<Input value={props.form.maxDurationSeconds} inputMode="numeric" onChange={(event) => patch({ maxDurationSeconds: event.target.value })} />
|
||||
<span className="runtimeFieldHint">从任务开始执行计时,超过后不再继续重试,默认 600 秒。</span>
|
||||
</Label>
|
||||
<KeywordField label="允许分类" value={props.form.allowCategories} onChange={(value) => patch({ allowCategories: value })} />
|
||||
<KeywordField label="拒绝分类" value={props.form.denyCategories} onChange={(value) => patch({ denyCategories: value })} />
|
||||
<KeywordField label="允许错误码" value={props.form.allowCodes} onChange={(value) => patch({ allowCodes: value })} />
|
||||
<KeywordField label="拒绝错误码" value={props.form.denyCodes} onChange={(value) => patch({ denyCodes: value })} />
|
||||
<KeywordField label="允许关键词" value={props.form.allowKeywords} onChange={(value) => patch({ allowKeywords: value })} />
|
||||
<KeywordField label="拒绝关键词" value={props.form.denyKeywords} onChange={(value) => patch({ denyKeywords: value })} />
|
||||
<KeywordField label="允许状态码" value={props.form.allowStatusCodes} onChange={(value) => patch({ allowStatusCodes: value })} />
|
||||
<KeywordField label="拒绝状态码" value={props.form.denyStatusCodes} onChange={(value) => patch({ denyStatusCodes: value })} />
|
||||
<FailoverCategoryRoutingEditor
|
||||
actions={props.form.failoverActions}
|
||||
allowCategories={props.form.allowCategories}
|
||||
denyCategories={props.form.denyCategories}
|
||||
onChange={(value) => patch(value)}
|
||||
/>
|
||||
<div className="spanTwo runnerSupplementalRules">
|
||||
<strong>补充触发条件(可自定义扩展)</strong>
|
||||
<small>这些条件只决定是否进入平台间切换;分类命中后的处理方式以上面的分组为准。</small>
|
||||
</div>
|
||||
<KeywordField label="补充触发错误码" value={props.form.allowCodes} onChange={(value) => patch({ allowCodes: value })} />
|
||||
<KeywordField label="排除触发错误码" value={props.form.denyCodes} onChange={(value) => patch({ denyCodes: value })} />
|
||||
<KeywordField label="补充触发关键词" value={props.form.allowKeywords} onChange={(value) => patch({ allowKeywords: value })} />
|
||||
<KeywordField label="排除触发关键词" value={props.form.denyKeywords} onChange={(value) => patch({ denyKeywords: value })} />
|
||||
<KeywordField label="补充触发状态码" value={props.form.allowStatusCodes} onChange={(value) => patch({ allowStatusCodes: value })} />
|
||||
<KeywordField label="排除触发状态码" value={props.form.denyStatusCodes} onChange={(value) => patch({ denyStatusCodes: value })} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -448,6 +489,65 @@ function KeywordField(props: { label: string; value: string[]; onChange: (value:
|
||||
);
|
||||
}
|
||||
|
||||
function FailoverCategoryRoutingEditor(props: {
|
||||
actions: Record<string, unknown>;
|
||||
allowCategories: string[];
|
||||
denyCategories: string[];
|
||||
onChange: (value: Pick<RunnerPolicyForm, 'allowCategories' | 'denyCategories' | 'failoverActions'>) => void;
|
||||
}) {
|
||||
const groups = failoverCategoryRoutingGroups(props.allowCategories, props.denyCategories, props.actions);
|
||||
const options = categoryOptions(props.allowCategories, props.denyCategories, Object.keys(props.actions));
|
||||
const updateGroup = (group: string, value: string[]) => {
|
||||
props.onChange(updateFailoverCategoryRouting(props.allowCategories, props.denyCategories, props.actions, group, value));
|
||||
};
|
||||
return (
|
||||
<div className="spanTwo runnerActionMatrix">
|
||||
<div className="runnerActionIntro">
|
||||
<strong>错误分类处理方式(一般保持默认无需修改)</strong>
|
||||
<small>每个错误分类只放在一个分组;前三组会进入平台间切换,拒绝轮转不会尝试后续平台。</small>
|
||||
</div>
|
||||
<div className="runnerActionGrid">
|
||||
{failoverActionDefinitions.map((action) => (
|
||||
<label className="runnerActionGroup" key={action.value} data-action={action.value}>
|
||||
<span>
|
||||
<strong>{action.title}</strong>
|
||||
<small>{action.description}</small>
|
||||
</span>
|
||||
<AntSelect
|
||||
allowClear
|
||||
className="runtimeTagInput"
|
||||
maxTagCount="responsive"
|
||||
mode="tags"
|
||||
options={options}
|
||||
placeholder="输入错误分类后回车"
|
||||
tokenSeparators={[',', '\n']}
|
||||
value={groups[action.value] ?? []}
|
||||
onChange={(value) => updateGroup(action.value, value)}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<label className="runnerActionGroup" data-action="deny">
|
||||
<span>
|
||||
<strong>拒绝轮转</strong>
|
||||
<small>这些错误分类不会尝试后续平台,当前任务直接失败。</small>
|
||||
</span>
|
||||
<AntSelect
|
||||
allowClear
|
||||
className="runtimeTagInput"
|
||||
maxTagCount="responsive"
|
||||
mode="tags"
|
||||
options={options}
|
||||
placeholder="输入错误分类后回车"
|
||||
tokenSeparators={[',', '\n']}
|
||||
value={groups.deny ?? []}
|
||||
onChange={(value) => updateGroup('deny', value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function runnerPolicyToForm(policy: GatewayRunnerPolicy | null): RunnerPolicyForm {
|
||||
const failover = readObject(policy?.failoverPolicy);
|
||||
const hardStop = readObject(policy?.hardStopPolicy);
|
||||
@@ -499,7 +599,7 @@ function runnerFormToPayload(form: RunnerPolicyForm): GatewayRunnerPolicyUpsertR
|
||||
denyKeywords: cleanTags(form.denyKeywords),
|
||||
allowStatusCodes: parseNumberTags(form.allowStatusCodes),
|
||||
denyStatusCodes: parseNumberTags(form.denyStatusCodes),
|
||||
actions: Object.keys(form.failoverActions).length > 0 ? form.failoverActions : defaultFailoverActions(),
|
||||
actions: normalizedFailoverActions(form),
|
||||
},
|
||||
hardStopPolicy: {
|
||||
enabled: form.hardStopEnabled,
|
||||
@@ -552,6 +652,97 @@ function defaultFailoverActions(): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function failoverCategoryRoutingGroups(allowCategories: string[], denyCategories: string[], actions: Record<string, unknown>) {
|
||||
const actionSet = new Set(failoverActionDefinitions.map((item) => item.value));
|
||||
const assignments = new Map<string, string>();
|
||||
for (const category of cleanTags(allowCategories)) {
|
||||
const rawAction = actions[category];
|
||||
const action = typeof rawAction === 'string' ? rawAction : '';
|
||||
assignments.set(category, actionSet.has(action as (typeof failoverActionDefinitions)[number]['value']) ? action : 'next');
|
||||
}
|
||||
for (const [category, rawAction] of Object.entries(actions)) {
|
||||
if (assignments.has(category)) continue;
|
||||
const action = typeof rawAction === 'string' ? rawAction : '';
|
||||
if (action === 'stop') {
|
||||
assignments.set(category, 'deny');
|
||||
} else if (actionSet.has(action as (typeof failoverActionDefinitions)[number]['value'])) {
|
||||
assignments.set(category, action);
|
||||
}
|
||||
}
|
||||
for (const category of cleanTags(denyCategories)) {
|
||||
assignments.set(category, 'deny');
|
||||
}
|
||||
const groups: Record<string, string[]> = {};
|
||||
for (const [category, group] of assignments.entries()) {
|
||||
groups[group] = [...(groups[group] ?? []), category];
|
||||
}
|
||||
for (const key of [...failoverActionDefinitions.map((item) => item.value), 'deny']) {
|
||||
groups[key] = cleanTags(groups[key] ?? []);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function updateFailoverCategoryRouting(
|
||||
allowCategories: string[],
|
||||
denyCategories: string[],
|
||||
actions: Record<string, unknown>,
|
||||
group: string,
|
||||
value: string[],
|
||||
): Pick<RunnerPolicyForm, 'allowCategories' | 'denyCategories' | 'failoverActions'> {
|
||||
const groups = failoverCategoryRoutingGroups(allowCategories, denyCategories, actions);
|
||||
groups[group] = cleanTags(value);
|
||||
const nextActions: Record<string, unknown> = {};
|
||||
const knownCategories = new Set<string>();
|
||||
const nextAllowCategories: string[] = [];
|
||||
for (const action of failoverActionDefinitions) {
|
||||
for (const category of cleanTags(groups[action.value] ?? [])) {
|
||||
knownCategories.add(category);
|
||||
nextAllowCategories.push(category);
|
||||
if (action.value !== 'next') {
|
||||
nextActions[category] = action.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextDenyCategories = cleanTags(groups.deny ?? []);
|
||||
for (const category of nextDenyCategories) {
|
||||
knownCategories.add(category);
|
||||
}
|
||||
for (const [category, rawAction] of Object.entries(actions)) {
|
||||
if (!knownCategories.has(category) && typeof rawAction === 'string' && rawAction !== 'next' && rawAction !== 'stop') {
|
||||
nextActions[category] = rawAction;
|
||||
}
|
||||
}
|
||||
return {
|
||||
allowCategories: cleanTags(nextAllowCategories),
|
||||
denyCategories: nextDenyCategories,
|
||||
failoverActions: nextActions,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizedFailoverActions(form: RunnerPolicyForm) {
|
||||
const groups = failoverCategoryRoutingGroups(form.allowCategories, form.denyCategories, form.failoverActions);
|
||||
const nextActions: Record<string, unknown> = {};
|
||||
for (const action of failoverActionDefinitions) {
|
||||
if (action.value === 'next') continue;
|
||||
for (const category of cleanTags(groups[action.value] ?? [])) {
|
||||
nextActions[category] = action.value;
|
||||
}
|
||||
}
|
||||
return nextActions;
|
||||
}
|
||||
|
||||
function categoryOptions(...values: string[][]) {
|
||||
const knownValues = new Set(failoverCategoryOptions.map((option) => option.value));
|
||||
const options = [...failoverCategoryOptions];
|
||||
for (const value of values.flat()) {
|
||||
const tag = String(value).trim();
|
||||
if (!tag || knownValues.has(tag)) continue;
|
||||
knownValues.add(tag);
|
||||
options.push({ label: tag, value: tag });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
|
||||
const rateRules = Array.isArray(policy.rateLimitPolicy?.rules) ? policy.rateLimitPolicy.rules : [];
|
||||
const retry = readObject(policy.retryPolicy);
|
||||
|
||||
Reference in New Issue
Block a user