mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(routing): consolidate scheduling strategy configuration
This commit is contained in:
@@ -13,9 +13,9 @@ pub use actions::{
|
||||
};
|
||||
pub use conditions::{RoutingCondition, RoutingConditionContext, RoutingConditionOp};
|
||||
pub use model::{
|
||||
RoutingDefaultPolicy, RoutingGroupBinding, RoutingGroupBindingSubject, RoutingGroupConfig,
|
||||
RoutingGroupRecord, RoutingGroupVersionRecord, RoutingModelPolicy, RoutingPoolPolicyOverride,
|
||||
RoutingRule, RoutingSchedulingPreset, DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
RoutingDefaultPolicy, RoutingExecutionPolicy, RoutingGroupBinding, RoutingGroupBindingSubject,
|
||||
RoutingGroupConfig, RoutingGroupRecord, RoutingGroupVersionRecord, RoutingModelPolicy,
|
||||
RoutingPoolPolicyOverride, RoutingRule, RoutingSchedulingPreset, DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
};
|
||||
pub use mutations::{
|
||||
apply_json_patch_operations, validate_header_patch, validate_json_patch_operations,
|
||||
|
||||
@@ -27,6 +27,47 @@ pub struct RoutingPoolPolicyOverride {
|
||||
/// failing over: one retry on the same key.
|
||||
pub const DEFAULT_STICKY_KEY_ATTEMPTS: u32 = 2;
|
||||
|
||||
/// Request-independent execution behaviours selected by a routing strategy.
|
||||
///
|
||||
/// These flags deliberately live beside scheduling rather than in provider
|
||||
/// transport configuration. A resolved policy is snapshotted for the request
|
||||
/// and can therefore be consumed by execution without rereading mutable
|
||||
/// system settings.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
|
||||
pub struct RoutingExecutionPolicy {
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub enable_cf_heartbeat: bool,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub cyber_continue_failover: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RoutingExecutionPolicy {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize, Default)]
|
||||
struct LegacyCompatibleExecutionPolicy {
|
||||
#[serde(default)]
|
||||
enable_cf_heartbeat: bool,
|
||||
#[serde(default)]
|
||||
enable_openai_image_sync_heartbeat: bool,
|
||||
#[serde(default)]
|
||||
enable_standard_text_sync_heartbeat: bool,
|
||||
#[serde(default)]
|
||||
cyber_continue_failover: bool,
|
||||
}
|
||||
|
||||
let value = LegacyCompatibleExecutionPolicy::deserialize(deserializer)?;
|
||||
Ok(Self {
|
||||
enable_cf_heartbeat: value.enable_cf_heartbeat
|
||||
|| value.enable_openai_image_sync_heartbeat
|
||||
|| value.enable_standard_text_sync_heartbeat,
|
||||
cyber_continue_failover: value.cyber_continue_failover,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RoutingDefaultPolicy {
|
||||
#[serde(default)]
|
||||
@@ -40,6 +81,10 @@ pub struct RoutingDefaultPolicy {
|
||||
/// `0` and `1` both mean no same-key retry.
|
||||
#[serde(default = "default_sticky_key_attempts")]
|
||||
pub sticky_key_attempts: u32,
|
||||
/// Strategy-scoped execution behaviour. Flattened for a stable JSON
|
||||
/// shape and backwards-compatible migration from system settings.
|
||||
#[serde(flatten)]
|
||||
pub execution_policy: RoutingExecutionPolicy,
|
||||
}
|
||||
|
||||
impl Default for RoutingDefaultPolicy {
|
||||
@@ -49,6 +94,7 @@ impl Default for RoutingDefaultPolicy {
|
||||
scheduling_mode: RoutingSchedulingMode::default(),
|
||||
keep_priority_on_conversion: false,
|
||||
sticky_key_attempts: DEFAULT_STICKY_KEY_ATTEMPTS,
|
||||
execution_policy: RoutingExecutionPolicy::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +103,10 @@ fn default_sticky_key_attempts() -> u32 {
|
||||
DEFAULT_STICKY_KEY_ATTEMPTS
|
||||
}
|
||||
|
||||
fn is_false(value: &bool) -> bool {
|
||||
!*value
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RoutingModelPolicy {
|
||||
pub model: String,
|
||||
@@ -100,8 +150,8 @@ pub struct RoutingRule {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RoutingGroupConfig {
|
||||
#[serde(default)]
|
||||
pub allowed_models: Vec<String>,
|
||||
/// The default policy is global for the selected strategy group. Model
|
||||
/// differences are expressed through `model_policies` and `rules`.
|
||||
#[serde(default)]
|
||||
pub default_policy: RoutingDefaultPolicy,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -8,7 +8,9 @@ use crate::actions::{
|
||||
RoutingAction, RoutingRulePhase, RoutingSchedulingMode, RoutingSetPriorityMode,
|
||||
};
|
||||
use crate::conditions::RoutingConditionContext;
|
||||
use crate::model::{RoutingGroupConfig, RoutingModelPolicy, RoutingPoolPolicyOverride};
|
||||
use crate::model::{
|
||||
RoutingExecutionPolicy, RoutingGroupConfig, RoutingModelPolicy, RoutingPoolPolicyOverride,
|
||||
};
|
||||
use crate::mutations::{validate_header_patch, validate_json_patch_operations, MutationPlan};
|
||||
use crate::ranking::RankingOverlay;
|
||||
use crate::validation::validate_routing_group_config;
|
||||
@@ -17,7 +19,7 @@ use crate::validation::validate_routing_group_config;
|
||||
pub enum RoutingPolicyError {
|
||||
#[error("routing group config is invalid: {0}")]
|
||||
InvalidConfig(String),
|
||||
#[error("model is not allowed by routing group: {0}")]
|
||||
#[error("model is not allowed by routing rule: {0}")]
|
||||
ModelNotAllowed(String),
|
||||
#[error("mutation action is invalid: {0}")]
|
||||
InvalidMutation(String),
|
||||
@@ -60,6 +62,8 @@ pub struct ResolvedRoutingPolicy {
|
||||
/// See `RoutingDefaultPolicy::sticky_key_attempts`.
|
||||
#[serde(default = "default_sticky_key_attempts")]
|
||||
pub sticky_key_attempts: u32,
|
||||
#[serde(flatten)]
|
||||
pub execution_policy: RoutingExecutionPolicy,
|
||||
pub ranking_overlay: RankingOverlay,
|
||||
pub mutation_plan: MutationPlan,
|
||||
#[serde(default)]
|
||||
@@ -75,14 +79,6 @@ pub fn resolve_routing_policy(
|
||||
validate_routing_group_config(config)
|
||||
.map_err(|error| RoutingPolicyError::InvalidConfig(error.to_string()))?;
|
||||
|
||||
if !model_allowed(&config.allowed_models, input.requested_model)
|
||||
&& !model_allowed(&config.allowed_models, input.resolved_model)
|
||||
{
|
||||
return Err(RoutingPolicyError::ModelNotAllowed(
|
||||
input.requested_model.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut policy = ResolvedRoutingPolicy {
|
||||
group_id: input.group_id.map(str::to_string),
|
||||
group_version: input.group_version,
|
||||
@@ -93,6 +89,7 @@ pub fn resolve_routing_policy(
|
||||
scheduling_mode: config.default_policy.scheduling_mode,
|
||||
keep_priority_on_conversion: config.default_policy.keep_priority_on_conversion,
|
||||
sticky_key_attempts: config.default_policy.sticky_key_attempts,
|
||||
execution_policy: config.default_policy.execution_policy,
|
||||
ranking_overlay: RankingOverlay::default(),
|
||||
mutation_plan: MutationPlan::default(),
|
||||
pool_policy_overrides: BTreeMap::new(),
|
||||
@@ -322,7 +319,6 @@ mod tests {
|
||||
#[test]
|
||||
fn resolves_model_policy_and_matching_rule() {
|
||||
let config = RoutingGroupConfig {
|
||||
allowed_models: vec!["gpt-*".to_string()],
|
||||
default_policy: RoutingDefaultPolicy::default(),
|
||||
model_policies: vec![RoutingModelPolicy {
|
||||
model: "gpt-5".to_string(),
|
||||
@@ -389,14 +385,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_allowlist_keeps_default_policy_for_models_without_an_override() {
|
||||
fn default_policy_applies_to_models_without_an_override() {
|
||||
let config = RoutingGroupConfig {
|
||||
allowed_models: vec![],
|
||||
default_policy: RoutingDefaultPolicy {
|
||||
priority_mode: RoutingSetPriorityMode::GlobalKey,
|
||||
scheduling_mode: RoutingSchedulingMode::LoadBalance,
|
||||
keep_priority_on_conversion: true,
|
||||
sticky_key_attempts: 3,
|
||||
execution_policy: Default::default(),
|
||||
},
|
||||
model_policies: vec![RoutingModelPolicy {
|
||||
model: "special-model".to_string(),
|
||||
@@ -471,6 +467,38 @@ mod tests {
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_group_model_allowlist_is_ignored() {
|
||||
let config: RoutingGroupConfig = serde_json::from_value(json!({
|
||||
"allowed_models": ["gpt-5"],
|
||||
"default_policy": {
|
||||
"priority_mode": "provider",
|
||||
"scheduling_mode": "cache_affinity"
|
||||
},
|
||||
"model_policies": [],
|
||||
"rules": []
|
||||
}))
|
||||
.expect("legacy routing config should remain readable");
|
||||
|
||||
resolve_routing_policy(
|
||||
&config,
|
||||
RoutingPolicyInput {
|
||||
group_id: Some("group-1"),
|
||||
group_version: Some(1),
|
||||
selection_source: "system_default",
|
||||
requested_model: "claude-sonnet",
|
||||
resolved_model: "claude-sonnet",
|
||||
api_format: "openai:chat",
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
headers: &json!({}),
|
||||
body: &json!({}),
|
||||
phase: RoutingRulePhase::ClientRequest,
|
||||
},
|
||||
)
|
||||
.expect("the legacy allowlist must not reject another model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticky_key_attempts_defaults_to_two_and_can_be_overridden_by_rule() {
|
||||
let default_config = RoutingGroupConfig::default();
|
||||
@@ -541,41 +569,9 @@ mod tests {
|
||||
assert_eq!(policy.sticky_key_attempts, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_disallowed_model() {
|
||||
let config = RoutingGroupConfig {
|
||||
allowed_models: vec!["gpt-5".to_string()],
|
||||
..RoutingGroupConfig::default()
|
||||
};
|
||||
|
||||
let err = resolve_routing_policy(
|
||||
&config,
|
||||
RoutingPolicyInput {
|
||||
group_id: None,
|
||||
group_version: None,
|
||||
selection_source: "test",
|
||||
requested_model: "claude",
|
||||
resolved_model: "claude",
|
||||
api_format: "openai:chat",
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
headers: &json!({}),
|
||||
body: &json!({}),
|
||||
phase: RoutingRulePhase::ClientRequest,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
RoutingPolicyError::ModelNotAllowed("claude".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restrict_model_action_rejects_matching_request() {
|
||||
let config = RoutingGroupConfig {
|
||||
allowed_models: vec!["*".to_string()],
|
||||
rules: vec![RoutingRule {
|
||||
id: "restrict".to_string(),
|
||||
priority: 1,
|
||||
|
||||
Reference in New Issue
Block a user