use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::actions::{ RoutingAction, RoutingRulePhase, RoutingSchedulingMode, RoutingSetPriorityMode, }; use crate::conditions::RoutingCondition; use crate::RoutingFailoverRules; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingSchedulingPreset { pub preset: String, #[serde(default = "default_true")] pub enabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub mode: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RoutingPoolPolicyOverride { #[serde(default)] pub scheduling_presets: Vec, } /// Default number of attempts on the first-ranked (sticky) candidate before /// 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, 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, #[serde(default, skip_serializing_if = "is_false")] pub cancel_on_client_disconnect: bool, #[serde(default)] pub max_transfer_count: u64, #[serde(default)] pub max_transfer_timeout_seconds: u64, #[serde(default)] pub failover_rules: RoutingFailoverRules, } impl<'de> Deserialize<'de> for RoutingExecutionPolicy { fn deserialize(deserializer: D) -> Result 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, #[serde(default)] cancel_on_client_disconnect: bool, #[serde(default)] max_transfer_count: u64, #[serde(default)] max_transfer_timeout_seconds: u64, #[serde(default)] failover_rules: RoutingFailoverRules, } 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, cancel_on_client_disconnect: value.cancel_on_client_disconnect, max_transfer_count: value.max_transfer_count, max_transfer_timeout_seconds: value.max_transfer_timeout_seconds, failover_rules: value.failover_rules, }) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingDefaultPolicy { #[serde(default)] pub priority_mode: RoutingSetPriorityMode, #[serde(default)] pub scheduling_mode: RoutingSchedulingMode, #[serde(default)] pub keep_priority_on_conversion: bool, /// Total attempts on the first-ranked candidate before moving on. Later /// candidates always get a single attempt so failover keeps advancing. /// `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 { fn default() -> Self { Self { priority_mode: RoutingSetPriorityMode::default(), scheduling_mode: RoutingSchedulingMode::default(), keep_priority_on_conversion: false, sticky_key_attempts: DEFAULT_STICKY_KEY_ATTEMPTS, execution_policy: RoutingExecutionPolicy::default(), } } } fn default_sticky_key_attempts() -> u32 { DEFAULT_STICKY_KEY_ATTEMPTS } fn is_false(value: &bool) -> bool { !*value } #[cfg(test)] mod execution_policy_tests { use super::*; #[test] fn routing_failover_configuration_round_trips_and_validates() { let config: RoutingGroupConfig = serde_json::from_value(serde_json::json!({ "default_policy": { "max_transfer_count": 3, "max_transfer_timeout_seconds": 90, "failover_rules": { "success_failover_patterns": [{ "pattern": "(?i)capacity" }], "error_stop_patterns": [{ "status_codes": [400, 413] }] } } })) .unwrap(); crate::validate_routing_group_config(&config).unwrap(); assert_eq!(config.default_policy.execution_policy.max_transfer_count, 3); let value = serde_json::to_value(&config).unwrap(); assert_eq!(value["default_policy"]["max_transfer_timeout_seconds"], 90); assert_eq!( serde_json::from_value::(value).unwrap(), config ); } #[test] fn cancellation_defaults_off_and_round_trips_with_legacy_heartbeat() { let default: RoutingDefaultPolicy = serde_json::from_str("{}").unwrap(); assert!(!default.execution_policy.cancel_on_client_disconnect); let policy: RoutingDefaultPolicy = serde_json::from_value(serde_json::json!({ "cancel_on_client_disconnect": true, "enable_standard_text_sync_heartbeat": true })) .unwrap(); assert!(policy.execution_policy.cancel_on_client_disconnect); assert!(policy.execution_policy.enable_cf_heartbeat); let encoded = serde_json::to_value(&policy).unwrap(); assert_eq!(encoded["cancel_on_client_disconnect"], true); assert_eq!( serde_json::from_value::(encoded).unwrap(), policy ); assert!( serde_json::from_value::(serde_json::json!({ "cancel_on_client_disconnect": "true" })) .is_err() ); } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RoutingModelPolicy { pub model: String, #[serde(default)] pub allowed_providers: Vec, #[serde(default)] pub allowed_keys: Vec, #[serde(default)] pub provider_priority_overrides: BTreeMap, #[serde(default)] pub key_priority_overrides: BTreeMap, /// Key priority overrides scoped to one API format: `api_format -> key_id -> priority`. /// /// A key can serve several API formats and legacy `global_priority_by_format` /// ranks it independently per format. Entries here take precedence over /// `key_priority_overrides` when the candidate format matches. #[serde(default)] pub key_priority_overrides_by_format: BTreeMap>, #[serde(default)] pub pool_priority_overrides: BTreeMap, #[serde(default)] pub pool_policy_overrides: BTreeMap, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingRule { pub id: String, #[serde(default)] pub priority: i32, #[serde(default = "default_true")] pub enabled: bool, #[serde(default)] pub phase: RoutingRulePhase, #[serde(default)] pub conditions: RoutingCondition, #[serde(default)] pub actions: Vec, #[serde(default)] pub stop_processing: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RoutingGroupConfig { /// 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)] pub model_policies: Vec, #[serde(default)] pub rules: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingGroupRecord { pub id: String, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub enabled: bool, pub is_system_default: bool, pub config_json: Value, pub version: i64, pub created_at: i64, pub updated_at: i64, #[serde(default, skip_serializing_if = "Option::is_none")] pub published_at: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RoutingGroupBindingSubject { User, ApiKey, UserGroup, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingGroupBinding { pub id: String, pub group_id: String, pub subject_type: RoutingGroupBindingSubject, pub subject_id: String, pub is_default: bool, pub allow_explicit_select: bool, pub created_at: i64, pub updated_at: i64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoutingGroupVersionRecord { pub id: String, pub group_id: String, pub version: i64, pub config_json: Value, pub created_at: i64, #[serde(default, skip_serializing_if = "Option::is_none")] pub created_by: Option, } fn default_true() -> bool { true }