diff --git a/crates/brightstaff/src/handlers/routing_service.rs b/crates/brightstaff/src/handlers/routing_service.rs index 0411f90a..6566a324 100644 --- a/crates/brightstaff/src/handlers/routing_service.rs +++ b/crates/brightstaff/src/handlers/routing_service.rs @@ -197,6 +197,7 @@ async fn routing_decision_inner( #[cfg(test)] mod tests { use super::*; + use common::configuration::SelectionPreference; fn make_chat_body(extra_fields: &str) -> Vec { let extra = if extra_fields.is_empty() { @@ -264,6 +265,61 @@ mod tests { assert!(cleaned_json.get("routing_preferences").is_none()); } + #[test] + fn extract_routing_policy_prefer_null_defaults_to_none() { + let policy = r#""routing_preferences": [ + { + "name": "coding", + "description": "code generation, writing functions, debugging", + "models": ["openai/gpt-4o", "openai/gpt-4o-mini"], + "selection_policy": {"prefer": null} + } + ]"#; + let body = make_chat_body(policy); + let (_cleaned, prefs) = extract_routing_policy(&body).unwrap(); + + let prefs = prefs.expect("should parse routing_preferences when prefer is null"); + assert_eq!(prefs.len(), 1); + assert_eq!(prefs[0].selection_policy.prefer, SelectionPreference::None); + } + + #[test] + fn extract_routing_policy_selection_policy_missing_defaults_to_none() { + let policy = r#""routing_preferences": [ + { + "name": "coding", + "description": "code generation, writing functions, debugging", + "models": ["openai/gpt-4o", "openai/gpt-4o-mini"] + } + ]"#; + let body = make_chat_body(policy); + let (_cleaned, prefs) = extract_routing_policy(&body).unwrap(); + + let prefs = + prefs.expect("should parse routing_preferences when selection_policy is missing"); + assert_eq!(prefs.len(), 1); + assert_eq!(prefs[0].selection_policy.prefer, SelectionPreference::None); + } + + #[test] + fn extract_routing_policy_prefer_empty_string_defaults_to_none() { + let policy = r#""routing_preferences": [ + { + "name": "coding", + "description": "code generation, writing functions, debugging", + "models": ["openai/gpt-4o", "openai/gpt-4o-mini"], + "selection_policy": {"prefer": ""} + } + ]"#; + let body = make_chat_body(policy); + let (_cleaned, prefs) = extract_routing_policy(&body).unwrap(); + + let prefs = + prefs.expect("should parse routing_preferences when selection_policy.prefer is empty"); + assert_eq!(prefs.len(), 1); + assert_eq!(prefs[0].selection_policy.prefer, SelectionPreference::None); + } + #[test] fn routing_decision_response_serialization() { let response = RoutingDecisionResponse { diff --git a/crates/common/src/configuration.rs b/crates/common/src/configuration.rs index 653f4d8c..ac95185b 100644 --- a/crates/common/src/configuration.rs +++ b/crates/common/src/configuration.rs @@ -1,5 +1,5 @@ use hermesllm::apis::openai::{ModelDetail, ModelObject, Models}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; use std::fmt::Display; @@ -111,14 +111,25 @@ pub enum SelectionPreference { Fastest, /// Return models in the same order they were defined — no reordering. #[default] + #[serde(alias = "")] None, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SelectionPolicy { + #[serde(default, deserialize_with = "deserialize_selection_preference")] pub prefer: SelectionPreference, } +fn deserialize_selection_preference<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + Ok(Option::::deserialize(deserializer)?.unwrap_or_default()) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TopLevelRoutingPreference { pub name: String,