mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 22:20:19 +08:00
feat(routing): move sticky-key retries into routing policy with lazy attempts
Replace the provider/endpoint max_retries fields as the source of same-key retries with a routing policy setting, sticky_key_attempts (default 2). Only the first-ranked candidate is retried on the same key; every failover candidate gets a single attempt so failover keeps advancing instead of retrying each fallback key. Materialize exactly one attempt per candidate and derive same-key retries in the attempt loop after a candidate-scoped failure, so the retry budget no longer inflates up-front materialization and needs no upper bound. The budget travels in the report context; retries reuse the plan with a fresh candidate id and incremented retry index. Pool groups only retry their first key within the retry-index stride. Expose the setting in the routing profile editor and the set_scheduling rule action, and drop the max_retries input from the provider form.
This commit is contained in:
@@ -155,17 +155,6 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>{{ legacyT('最大重试次数') }}</Label>
|
||||
<Input
|
||||
:model-value="form.max_retries ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999"
|
||||
:placeholder="legacyT('默认 2')"
|
||||
@update:model-value="(v) => form.max_retries = parseNumberInput(v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 超时配置 -->
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getModelScheduling,
|
||||
modelSchedulingRuleId,
|
||||
normalizeRoutingGroupConfig,
|
||||
normalizeStickyKeyAttempts,
|
||||
parseAllowedModelsInput,
|
||||
removePerModelRoutingConfig,
|
||||
resolveModelKeyPriorityOverride,
|
||||
@@ -77,6 +78,19 @@ describe('routingPolicy', () => {
|
||||
expect(policy.key_priority_overrides).toEqual({})
|
||||
})
|
||||
|
||||
it('defaults sticky key attempts to 2 and normalizes invalid values', () => {
|
||||
expect(createEmptyRoutingGroupConfig().default_policy.sticky_key_attempts).toBe(2)
|
||||
expect(normalizeRoutingGroupConfig({}).default_policy.sticky_key_attempts).toBe(2)
|
||||
expect(normalizeRoutingGroupConfig({
|
||||
default_policy: { priority_mode: 'provider', scheduling_mode: 'cache_affinity', keep_priority_on_conversion: false, sticky_key_attempts: 3 },
|
||||
}).default_policy.sticky_key_attempts).toBe(3)
|
||||
expect(normalizeStickyKeyAttempts('5')).toBe(5)
|
||||
expect(normalizeStickyKeyAttempts(-1)).toBe(2)
|
||||
expect(normalizeStickyKeyAttempts('abc')).toBe(2)
|
||||
expect(normalizeStickyKeyAttempts(500)).toBe(99)
|
||||
expect(getModelScheduling(createEmptyRoutingGroupConfig(), 'gpt-5').sticky_key_attempts).toBe(2)
|
||||
})
|
||||
|
||||
it('keeps key priority overrides independent per api format', () => {
|
||||
let config = setModelKeyPriorityOverridesForFormat(
|
||||
createEmptyRoutingGroupConfig(),
|
||||
|
||||
@@ -3,10 +3,15 @@ export type RoutingSchedulingMode = 'fixed_order' | 'cache_affinity' | 'load_bal
|
||||
export type RoutingRulePhase = 'client_request' | 'provider_request'
|
||||
export type RoutingSortingScope = 'unified' | 'per_model'
|
||||
|
||||
/** 首个候选(粘性 Key)的总尝试次数默认值:失败后同 Key 重试 1 次 */
|
||||
export const DEFAULT_STICKY_KEY_ATTEMPTS = 2
|
||||
|
||||
export interface RoutingDefaultPolicy {
|
||||
priority_mode: RoutingPriorityMode
|
||||
scheduling_mode: RoutingSchedulingMode
|
||||
keep_priority_on_conversion: boolean
|
||||
/** 首个候选的总尝试次数;后续候选始终只尝试 1 次。0 或 1 表示不重试 */
|
||||
sticky_key_attempts: number
|
||||
}
|
||||
|
||||
export interface RoutingPoolSchedulingPreset {
|
||||
@@ -51,6 +56,7 @@ export interface RoutingSetSchedulingAction {
|
||||
type: 'set_scheduling'
|
||||
priority_mode: RoutingPriorityMode
|
||||
scheduling_mode: RoutingSchedulingMode
|
||||
sticky_key_attempts?: number
|
||||
}
|
||||
|
||||
export interface RoutingGroupConfig {
|
||||
@@ -70,12 +76,19 @@ export function createEmptyRoutingGroupConfig(): RoutingGroupConfig {
|
||||
priority_mode: 'provider',
|
||||
scheduling_mode: 'cache_affinity',
|
||||
keep_priority_on_conversion: false,
|
||||
sticky_key_attempts: DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
},
|
||||
model_policies: [],
|
||||
rules: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeStickyKeyAttempts(value: unknown): number {
|
||||
const parsed = Math.trunc(Number(value))
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_STICKY_KEY_ATTEMPTS
|
||||
return Math.min(parsed, 99)
|
||||
}
|
||||
|
||||
export function createEmptyModelPolicy(model = ''): RoutingModelPolicy {
|
||||
return {
|
||||
model,
|
||||
@@ -97,6 +110,9 @@ export function normalizeRoutingGroupConfig(value: Partial<RoutingGroupConfig> |
|
||||
default_policy: {
|
||||
...base.default_policy,
|
||||
...(value?.default_policy ?? {}),
|
||||
sticky_key_attempts: normalizeStickyKeyAttempts(
|
||||
value?.default_policy?.sticky_key_attempts ?? DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
),
|
||||
},
|
||||
model_policies: Array.isArray(value?.model_policies)
|
||||
? value.model_policies.map(policy => ({
|
||||
@@ -411,6 +427,7 @@ export function getModelScheduling(
|
||||
priority_mode: action?.priority_mode ?? normalized.default_policy.priority_mode,
|
||||
scheduling_mode: action?.scheduling_mode ?? normalized.default_policy.scheduling_mode,
|
||||
keep_priority_on_conversion: normalized.default_policy.keep_priority_on_conversion,
|
||||
sticky_key_attempts: action?.sticky_key_attempts ?? normalized.default_policy.sticky_key_attempts,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -977,6 +977,7 @@ const MOCK_ROUTING_GROUPS: MockRoutingGroup[] = [
|
||||
priority_mode: 'provider',
|
||||
scheduling_mode: 'cache_affinity',
|
||||
keep_priority_on_conversion: false,
|
||||
sticky_key_attempts: 2,
|
||||
},
|
||||
model_policies: [
|
||||
{
|
||||
|
||||
@@ -471,6 +471,27 @@
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-start gap-3 rounded-lg border border-border/60 px-3 py-2 text-sm"
|
||||
data-testid="sticky-key-attempts"
|
||||
>
|
||||
<Input
|
||||
:model-value="stickyKeyAttempts"
|
||||
type="number"
|
||||
min="0"
|
||||
max="99"
|
||||
class="w-20 shrink-0"
|
||||
:disabled="saving"
|
||||
aria-label="粘性 Key 尝试次数"
|
||||
@update:model-value="updateStickyKeyAttempts"
|
||||
/>
|
||||
<span class="min-w-0">
|
||||
<span class="block font-medium">粘性 Key 尝试次数</span>
|
||||
<span class="mt-0.5 block text-xs text-muted-foreground">
|
||||
首个候选(缓存亲和命中的 Key)的总尝试次数。2 表示失败后同 Key 重试 1 次再转移,避免偶发错误破坏缓存;0 或 1 表示不重试。转移后的候选始终只尝试 1 次。
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<RoutingPriorityPolicyEditor
|
||||
@@ -780,6 +801,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
|
||||
import { AlertDialog } from '@/components/common'
|
||||
import {
|
||||
DEFAULT_ROUTING_POLICY_MODEL,
|
||||
DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
allowedModelsMirrorPerModelPolicies,
|
||||
clearAllowedModels,
|
||||
copyPerModelRoutingConfig,
|
||||
@@ -790,6 +812,7 @@ import {
|
||||
isGeneratedModelSchedulingRule,
|
||||
modelSchedulingRuleId,
|
||||
normalizeRoutingGroupConfig,
|
||||
normalizeStickyKeyAttempts,
|
||||
removePerModelRoutingConfig,
|
||||
routingModelScopeLabel,
|
||||
savePerModelRoutingConfig,
|
||||
@@ -895,6 +918,9 @@ const firstStepSchedulingMode = computed<RoutingSchedulingMode>(() => {
|
||||
const keepPriorityOnConversion = computed<boolean>(() => (
|
||||
draft.value?.config_json.default_policy.keep_priority_on_conversion ?? false
|
||||
))
|
||||
const stickyKeyAttempts = computed<number>(() => (
|
||||
draft.value?.config_json.default_policy.sticky_key_attempts ?? DEFAULT_STICKY_KEY_ATTEMPTS
|
||||
))
|
||||
const allowedModelsLookLikeLegacyMirror = computed(() => {
|
||||
return draft.value
|
||||
? allowedModelsMirrorPerModelPolicies(draft.value.config_json)
|
||||
@@ -1211,6 +1237,17 @@ function updateFirstStepSchedulingMode(mode: RoutingSchedulingMode): void {
|
||||
})
|
||||
}
|
||||
|
||||
function updateStickyKeyAttempts(value: string | number): void {
|
||||
if (!draft.value) return
|
||||
updateDraftConfig({
|
||||
...draft.value.config_json,
|
||||
default_policy: {
|
||||
...draft.value.config_json.default_policy,
|
||||
sticky_key_attempts: normalizeStickyKeyAttempts(value),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function updateKeepPriorityOnConversion(value: boolean): void {
|
||||
if (!draft.value) return
|
||||
updateDraftConfig({
|
||||
|
||||
@@ -215,6 +215,7 @@ function routingGroup(
|
||||
priority_mode: 'provider',
|
||||
scheduling_mode: 'cache_affinity',
|
||||
keep_priority_on_conversion: false,
|
||||
sticky_key_attempts: 2,
|
||||
},
|
||||
model_policies: [],
|
||||
rules: [],
|
||||
|
||||
Reference in New Issue
Block a user