Files
Aether/crates/aether-routing-core/src/actions.rs
T
elky 7323d41fbe 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.
2026-09-02 20:48:40 +08:00

98 lines
2.6 KiB
Rust

use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingRulePhase {
#[default]
ClientRequest,
ProviderRequest,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingSetPriorityMode {
#[default]
Provider,
GlobalKey,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingSchedulingMode {
#[default]
CacheAffinity,
LoadBalance,
FixedOrder,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "op")]
pub enum RoutingJsonPatchOperation {
Add { path: String, value: Value },
Replace { path: String, value: Value },
Remove { path: String },
}
impl RoutingJsonPatchOperation {
pub fn path(&self) -> &str {
match self {
Self::Add { path, .. } | Self::Replace { path, .. } | Self::Remove { path } => path,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "op")]
pub enum RoutingHeaderPatch {
Set { name: String, value: String },
Remove { name: String },
}
impl RoutingHeaderPatch {
pub fn name(&self) -> &str {
match self {
Self::Set { name, .. } | Self::Remove { name } => name,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum RoutingAction {
RestrictModels {
models: Vec<String>,
},
RestrictProviders {
provider_ids: Vec<String>,
},
RestrictKeys {
key_ids: Vec<String>,
},
SetScheduling {
priority_mode: Option<RoutingSetPriorityMode>,
scheduling_mode: Option<RoutingSchedulingMode>,
keep_priority_on_conversion: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sticky_key_attempts: Option<u32>,
},
SetProviderPriority {
provider_id: String,
priority: i32,
},
SetKeyPriority {
key_id: String,
priority: i32,
/// When set, the override only applies to candidates served through
/// this API format; otherwise it applies to the key on every format.
#[serde(default, skip_serializing_if = "Option::is_none")]
api_format: Option<String>,
},
JsonPatchBody {
patch: Vec<RoutingJsonPatchOperation>,
},
PatchHeaders {
patch: Vec<RoutingHeaderPatch>,
},
}