Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions crates/switchyard-components-v2/src/profiles/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,43 @@ macro_rules! profile_types {
}

/// Parses a serialized profile body by dispatching to the owning config type.
///
/// The `type` discriminator is matched with `-` and `_` treated as
/// equivalent, so a snake_case name copied from a legacy route bundle
/// (e.g. `random_routing`) resolves to its v2 profile, and a hyphenated
/// spelling of an underscore type (e.g. `stage-router`) resolves too.
pub(crate) fn parse_profile_config(
profile_type: &str,
value: serde_json::Value,
env: &crate::config::ProfileBuildEnv<'_>,
) -> switchyard_core::Result<ProfileConfigEntry> {
match profile_type {
$(
<$config as crate::config::ProfileConfigDefinition>::PROFILE_TYPE => {
let config =
<$config as crate::config::ProfileConfigDefinition>::parse_profile_config(
value,
env,
)?;
Ok(ProfileConfigEntry::$config(Box::new(config)))
}
)+
other => Err(switchyard_core::SwitchyardError::InvalidConfig(format!(
"unknown profile type `{other}`"
))),
// Compare treating `-` and `_` as equal without allocating. Profile
// type names are short ASCII, so an equal-length per-byte scan that
// folds the separators is enough. Registered names must stay unique
// under this folding (they are today) so dispatch is unambiguous.
fn eq_ignoring_separators(a: &str, b: &str) -> bool {
a.len() == b.len()
&& a.bytes().zip(b.bytes()).all(|(x, y)| {
x == y || (matches!(x, b'-' | b'_') && matches!(y, b'-' | b'_'))
})
}

$(
if eq_ignoring_separators(
profile_type,
<$config as crate::config::ProfileConfigDefinition>::PROFILE_TYPE,
) {
let config =
<$config as crate::config::ProfileConfigDefinition>::parse_profile_config(
value,
env,
)?;
return Ok(ProfileConfigEntry::$config(Box::new(config)));
}
)+
Err(switchyard_core::SwitchyardError::InvalidConfig(format!(
"unknown profile type `{profile_type}`"
)))
}
};
}
Expand Down
67 changes: 67 additions & 0 deletions crates/switchyard-components-v2/tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,73 @@ profiles:
Ok(())
}

// `-` and `_` are equivalent in the `type` discriminator, so a name written with
// either separator resolves to the same v2 profile: a legacy `random_routing`
// spelling, and `stage-router` for the underscore-canonical stage router.
#[test]
fn profile_type_separators_are_normalized_during_resolution() -> Result<()> {
let input = r#"
endpoints:
nvidia:
api_key: ${NVIDIA_API_KEY}
base_url: https://inference-api.nvidia.com/v1

targets:
strong:
endpoint: nvidia
model: nvidia/moonshotai/kimi-k2.5
format: openai
weak:
endpoint: nvidia
model: nvidia/nvidia/nemotron-nano-9b-v2
format: openai
classifier:
endpoint: nvidia
model: nvidia/nvidia/nemotron-nano-9b-v2
format: openai

profiles:
snake-llm:
type: llm_routing
strong: strong
weak: weak
classifier: classifier
profile_name: coding_agent
hyphen-stage:
type: stage-router
capable: strong
efficient: weak
fallback_target_on_evict: strong
picker: capable_first
confidence_threshold: 0.7
snake-latency:
type: latency_service
latency_service_url: http://latency.local
targets: [strong, weak]
"#;

let plan = parse_yaml(input)?.resolve()?;

// Resolved profiles report the canonical type name regardless of the
// separator spelled in the config.
assert_eq!(
plan.profile_type(&ProfileId::new("snake-llm")?),
Some("llm-routing")
);
assert_eq!(
plan.profile_type(&ProfileId::new("hyphen-stage")?),
Some("stage_router")
);
assert_eq!(
plan.profile_type(&ProfileId::new("snake-latency")?),
Some("latency-service")
);

// The profiles build, not just parse.
assert_eq!(plan.build_profiles()?.len(), 3);
Ok(())
}

// Missing profile type discriminators should fail while parsing the document.
#[test]
fn missing_profile_type_is_rejected_during_document_parse() {
Expand Down
Loading