diff --git a/crates/brightstaff/src/handlers/routing_service.rs b/crates/brightstaff/src/handlers/routing_service.rs index 0411f90a..cab18dbc 100644 --- a/crates/brightstaff/src/handlers/routing_service.rs +++ b/crates/brightstaff/src/handlers/routing_service.rs @@ -27,8 +27,9 @@ pub fn extract_routing_policy( let routing_preferences = json_body .as_object_mut() .and_then(|o| o.remove("routing_preferences")) - .and_then( - |value| match serde_json::from_value::>(value) { + .and_then(|mut value| { + normalize_null_prefer_to_none(&mut value); + match serde_json::from_value::>(value) { Ok(prefs) => { info!( num_routes = prefs.len(), @@ -40,13 +41,43 @@ pub fn extract_routing_policy( warn!(error = %err, "failed to parse routing_preferences"); None } - }, - ); + } + }); let bytes = Bytes::from(serde_json::to_vec(&json_body).unwrap()); Ok((bytes, routing_preferences)) } +fn normalize_null_prefer_to_none(routing_preferences: &mut serde_json::Value) { + let Some(preferences) = routing_preferences.as_array_mut() else { + return; + }; + + for preference in preferences { + let Some(preference_obj) = preference.as_object_mut() else { + continue; + }; + + let Some(selection_policy) = preference_obj.get_mut("selection_policy") else { + continue; + }; + + let Some(policy_obj) = selection_policy.as_object_mut() else { + continue; + }; + + if policy_obj + .get("prefer") + .is_some_and(serde_json::Value::is_null) + { + policy_obj.insert( + "prefer".to_string(), + serde_json::Value::String("none".to_string()), + ); + } + } +} + #[derive(serde::Serialize)] struct RoutingDecisionResponse { /// Ranked model list — use first, fall back to next on 429/5xx. @@ -197,6 +228,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 +296,24 @@ 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 routing_decision_response_serialization() { let response = RoutingDecisionResponse {