mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
fix(combo): decouple failoverBeforeRetry same-model guard from the skipUpstreamRetry default
Audit found that DEFAULT_COMBO_CONFIG.failoverBeforeRetry has defaulted to true since before #10217 (predates #2417), and that value also feeds the independent skipUpstreamRetry mechanism (src/sse/handlers/chat.ts:859,1126). The previous commit on this branch flipped that default to false to fix the #10217 same-model retry guard, which silently disabled skipUpstreamRetry's own default-on behavior for every combo without an opt-in — a regression in the opposite direction (executor-level retries before the loop's own failover, changing latency/failure behavior). Revert the default back to true and decouple the two mechanisms instead: resolveComboConfig/resolveComboSetupConfig now also compute failoverBeforeRetryExplicit, true only when a cascade layer (combo/provider/ global) literally sets failoverBeforeRetry to true — not merely inherited from the default. The #10217 same-model retry guards in combo.ts (priority/ auto and round-robin loops) now read failoverBeforeRetryExplicit instead of config.failoverBeforeRetry, restoring opt-in-only behavior for that guard while the skipUpstreamRetry pass-through (config.failoverBeforeRetry at combo.ts:1297,2865) is untouched and keeps its historical default-on.
This commit is contained in:
@@ -1919,11 +1919,18 @@ export async function handleComboChat({
|
|||||||
// skip the same-model retry when `nextTarget` (computed above)
|
// skip the same-model retry when `nextTarget` (computed above)
|
||||||
// actually gives us somewhere to fail over to — with no sibling
|
// actually gives us somewhere to fail over to — with no sibling
|
||||||
// left, skipping just burns the last attempt for nothing.
|
// left, skipping just burns the last attempt for nothing.
|
||||||
|
//
|
||||||
|
// #10217 round-4 fix: this guard reads `failoverBeforeRetryExplicit`
|
||||||
|
// (opt-in only), NOT `config.failoverBeforeRetry` — that field
|
||||||
|
// defaults to true for the separate skipUpstreamRetry mechanism
|
||||||
|
// (see DEFAULT_COMBO_CONFIG comment in comboConfig.ts) and reading
|
||||||
|
// it here would silently skip the same-model retry for every combo,
|
||||||
|
// not just ones that explicitly opted in.
|
||||||
if (
|
if (
|
||||||
retry < maxRetries &&
|
retry < maxRetries &&
|
||||||
isTransient &&
|
isTransient &&
|
||||||
!providerExhausted &&
|
!providerExhausted &&
|
||||||
(!config.failoverBeforeRetry || !nextTarget)
|
(!config.failoverBeforeRetryExplicit || !nextTarget)
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!protectedPriorityTarget &&
|
!protectedPriorityTarget &&
|
||||||
@@ -2438,7 +2445,15 @@ async function handleRoundRobinCombo({
|
|||||||
}: HandleRoundRobinOptions): Promise<Response> {
|
}: HandleRoundRobinOptions): Promise<Response> {
|
||||||
const config = settings
|
const config = settings
|
||||||
? resolveComboConfig(combo, settings)
|
? resolveComboConfig(combo, settings)
|
||||||
: { ...getDefaultComboConfig(), ...(combo.config || {}) };
|
: {
|
||||||
|
...getDefaultComboConfig(),
|
||||||
|
...(combo.config || {}),
|
||||||
|
// See resolveComboConfig's failoverBeforeRetryExplicit comment in
|
||||||
|
// comboConfig.ts (no `settings` here, so only the combo's own config
|
||||||
|
// can opt in).
|
||||||
|
failoverBeforeRetryExplicit:
|
||||||
|
(combo.config as Record<string, unknown> | undefined)?.failoverBeforeRetry === true,
|
||||||
|
};
|
||||||
// #9158: clamp combo-level concurrency to a sane bound — a config carrying a
|
// #9158: clamp combo-level concurrency to a sane bound — a config carrying a
|
||||||
// huge or negative value would otherwise open an unbounded semaphore and
|
// huge or negative value would otherwise open an unbounded semaphore and
|
||||||
// flood targets (or deadlock at 0).
|
// flood targets (or deadlock at 0).
|
||||||
@@ -3163,12 +3178,14 @@ async function handleRoundRobinCombo({
|
|||||||
// just the lower-level skipUpstreamRetry mechanism. Only skip when
|
// just the lower-level skipUpstreamRetry mechanism. Only skip when
|
||||||
// `offset + 1 < modelCount` means a sibling target is actually left
|
// `offset + 1 < modelCount` means a sibling target is actually left
|
||||||
// in this rotation; with none left, skipping just wastes the attempt.
|
// in this rotation; with none left, skipping just wastes the attempt.
|
||||||
|
// #10217 round-4 fix: opt-in only — read failoverBeforeRetryExplicit,
|
||||||
|
// not config.failoverBeforeRetry (see comboConfig.ts comment).
|
||||||
const hasNextRrTarget = offset + 1 < modelCount;
|
const hasNextRrTarget = offset + 1 < modelCount;
|
||||||
if (
|
if (
|
||||||
retry < maxRetries &&
|
retry < maxRetries &&
|
||||||
isTransient &&
|
isTransient &&
|
||||||
!providerExhausted &&
|
!providerExhausted &&
|
||||||
(!config.failoverBeforeRetry || !hasNextRrTarget)
|
(!config.failoverBeforeRetryExplicit || !hasNextRrTarget)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,12 +126,23 @@ const DEFAULT_COMBO_CONFIG = {
|
|||||||
resetAwareWeeklyWeight: 0.65,
|
resetAwareWeeklyWeight: 0.65,
|
||||||
resetAwareTieBandPercent: 5,
|
resetAwareTieBandPercent: 5,
|
||||||
resetAwareExhaustionGuardPercent: 10,
|
resetAwareExhaustionGuardPercent: 10,
|
||||||
// Opt-in (#2417/#10217): when unset, transient errors retry the same model
|
// Historical default (predates #2417/#10217) — true. This value feeds TWO
|
||||||
// up to maxRetries before falling over to the next target — the historical
|
// independent mechanisms and must stay true-by-default for one of them:
|
||||||
// default. Defaulting this to true silently flipped that behavior for every
|
// 1. skipUpstreamRetry (src/sse/handlers/chat.ts:859,1126) — the
|
||||||
// combo that never touched the setting, breaking same-model retry semantics
|
// lower-level executor retry skip. Always default-on; changing this
|
||||||
// (round 4 base-red bisect: 06f41cda63 vs d2fd88dfbc).
|
// default flips that mechanism's behavior for every combo, not just
|
||||||
failoverBeforeRetry: false,
|
// opted-in ones.
|
||||||
|
// 2. The #10217 same-model retry guard in this file's combo.ts callers
|
||||||
|
// (priority/auto + round-robin loops) — meant to be OPT-IN only. That
|
||||||
|
// guard must NOT read this field directly; it consults the sibling
|
||||||
|
// `failoverBeforeRetryExplicit` flag computed below in
|
||||||
|
// resolveComboConfig/resolveComboSetupConfig, which is true only when
|
||||||
|
// an actual cascade layer (combo/provider/global) set the flag to
|
||||||
|
// true, not merely inherited from this default. See round-4 base-red
|
||||||
|
// bisect (06f41cda63 vs d2fd88dfbc) — flipping THIS default to false
|
||||||
|
// "fixed" mechanism 2 but silently broke mechanism 1's default-on
|
||||||
|
// behavior for every combo without an explicit opt-in.
|
||||||
|
failoverBeforeRetry: true,
|
||||||
// Feature 4985: configurable response-body validation predicate (per-combo). When set,
|
// Feature 4985: configurable response-body validation predicate (per-combo). When set,
|
||||||
// a 200 OK whose body fails the predicate fails over to the next target.
|
// a 200 OK whose body fails the predicate fails over to the next target.
|
||||||
responseValidation: undefined as ResponseValidationConfig | undefined,
|
responseValidation: undefined as ResponseValidationConfig | undefined,
|
||||||
@@ -289,15 +300,32 @@ export function resolveComboConfig(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cleanGlobal = clean(global);
|
||||||
|
const cleanProviderOverride = clean(providerOverride);
|
||||||
|
const cleanComboConfig = clean(comboConfig);
|
||||||
|
|
||||||
const merged = {
|
const merged = {
|
||||||
...DEFAULT_COMBO_CONFIG,
|
...DEFAULT_COMBO_CONFIG,
|
||||||
...clean(global),
|
...cleanGlobal,
|
||||||
...clean(providerOverride),
|
...cleanProviderOverride,
|
||||||
...clean(comboConfig),
|
...cleanComboConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #10217 round-4 fix: `failoverBeforeRetry` defaults to true (see comment on
|
||||||
|
// DEFAULT_COMBO_CONFIG above) and feeds two independent mechanisms. Callers
|
||||||
|
// that gate the OPT-IN same-model retry guard (combo.ts) must NOT read
|
||||||
|
// `merged.failoverBeforeRetry` directly — that stays true unless a layer
|
||||||
|
// explicitly disables it, which can't distinguish "inherited default" from
|
||||||
|
// "operator opted in". This flag is true only when some cascade layer
|
||||||
|
// literally set the value to true, i.e. a genuine opt-in.
|
||||||
|
const failoverBeforeRetryExplicit =
|
||||||
|
cleanComboConfig.failoverBeforeRetry === true ||
|
||||||
|
cleanProviderOverride.failoverBeforeRetry === true ||
|
||||||
|
cleanGlobal.failoverBeforeRetry === true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...merged,
|
...merged,
|
||||||
|
failoverBeforeRetryExplicit,
|
||||||
shadowRouting: {
|
shadowRouting: {
|
||||||
...DEFAULT_COMBO_CONFIG.shadowRouting,
|
...DEFAULT_COMBO_CONFIG.shadowRouting,
|
||||||
...(isRecord(global.shadowRouting) ? clean(global.shadowRouting) : {}),
|
...(isRecord(global.shadowRouting) ? clean(global.shadowRouting) : {}),
|
||||||
@@ -327,7 +355,14 @@ export function getDefaultComboConfig() {
|
|||||||
* return type is the single source of truth for ComboContext.config (combo/context.ts).
|
* return type is the single source of truth for ComboContext.config (combo/context.ts).
|
||||||
*/
|
*/
|
||||||
export function resolveComboSetupConfig(combo: ComboConfigLike, settings: ComboSettingsLike) {
|
export function resolveComboSetupConfig(combo: ComboConfigLike, settings: ComboSettingsLike) {
|
||||||
return settings
|
if (settings) return resolveComboConfig(combo, settings);
|
||||||
? resolveComboConfig(combo, settings)
|
const comboConfig = (combo?.config as Record<string, unknown>) || {};
|
||||||
: { ...getDefaultComboConfig(), ...((combo?.config as Record<string, unknown>) || {}) };
|
return {
|
||||||
|
...getDefaultComboConfig(),
|
||||||
|
...comboConfig,
|
||||||
|
// See resolveComboConfig's failoverBeforeRetryExplicit comment — same
|
||||||
|
// distinction applies here (no `settings`, so only the combo's own config
|
||||||
|
// can opt in).
|
||||||
|
failoverBeforeRetryExplicit: comboConfig.failoverBeforeRetry === true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user