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
15 changes: 10 additions & 5 deletions crates/switchyard-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1774,16 +1774,17 @@ targets = ["only"]
);
}

/// A successful judge call lands in the per-session routing snapshot under its
/// model id with the classifier tier, while routed calls stay off the observer's
/// log path — they are logged with terminal usage when the served response is
/// observed, so an append here would double count them.
/// The observer logs judge calls with their model IDs, classifier tier, and
/// recorded session and turn IDs. The served response logs routed calls with
/// their final usage, so logging them here would count each routed call twice.
#[test]
fn stats_observer_logs_judge_calls_to_the_routing_log() {
fn stats_observer_logs_judge_calls_to_the_routing_log() -> Result<(), Box<dyn std::error::Error>>
{
let dir = tempfile::tempdir().expect("temp dir");
let log = SharedRoutingLog::new(dir.path().join("routing.jsonl")).expect("routing log");
let mut headers = HeaderMap::new();
headers.insert("proxy_x_session_id", "session-1".parse().expect("header"));
headers.insert("x-switchyard-turn-id", "turn-1".parse()?);
let metadata = metadata_from_headers(headers);
let context = routing_log::RoutingLogContext::from_metadata(&metadata);
let observer = stats_observer(StatsAccumulator::default(), Some((log.clone(), context)));
Expand Down Expand Up @@ -1817,6 +1818,10 @@ targets = ["only"]
assert_eq!(snapshot["models"]["judge-model"]["prompt_tokens"], 100);
assert_eq!(snapshot["models"]["judge-model"]["completion_tokens"], 7);
assert!(snapshot["models"].get("routed-model").is_none());
let record: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&log.path)?)?;
assert_eq!(record["session_id"], "session-1");
assert_eq!(record["turn_id"], "turn-1");
Ok(())
}

#[derive(Clone)]
Expand Down
5 changes: 5 additions & 0 deletions crates/switchyard-server/src/routing_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl RoutingLog {
task: context.task.map(Cow::Owned),
trial_id: context.trial_id.map(Cow::Owned),
session_id: context.session_id.map(Cow::Owned),
turn_id: context.turn_id.map(Cow::Owned),
model: model.into(),
tier: tier.unwrap_or("").into(),
prompt_tokens: usage.prompt_tokens,
Expand Down Expand Up @@ -108,6 +109,7 @@ pub(crate) struct RoutingLogContext {
task: Option<String>,
trial_id: Option<String>,
session_id: Option<String>,
turn_id: Option<String>,
}

impl RoutingLogContext {
Expand All @@ -126,6 +128,7 @@ impl RoutingLogContext {
trial_id: headers
.and_then(|headers| nonempty_header(headers, TRIAL_ID_HEADER))
.map(str::to_string),
turn_id: metadata.turn_id.clone(),
session_id: metadata.session_id.clone().or_else(|| {
headers
.and_then(|headers| nonempty_header(headers, LEGACY_SESSION_ID_HEADER))
Expand Down Expand Up @@ -159,6 +162,8 @@ struct RoutingRecord<'a> {
trial_id: Option<Cow<'a, str>>,
#[serde(borrow)]
session_id: Option<Cow<'a, str>>,
#[serde(borrow)]
turn_id: Option<Cow<'a, str>>,
model: Cow<'a, str>,
tier: Cow<'a, str>,
prompt_tokens: u64,
Expand Down
5 changes: 5 additions & 0 deletions crates/switchyard-server/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4136,6 +4136,7 @@ async fn routing_log_prefers_canonical_and_preserves_legacy_fallback() -> TestRe
.uri("/v1/chat/completions")
.header("content-type", "application/json")
.header("x-switchyard-session-id", "canonical-session")
.header("x-switchyard-turn-id", "canonical-turn")
.header("proxy_x_session_id", "legacy-session")
.header("x-switchyard-origin", r#"custom-agent/"quoted"\path"#)
.body(Body::from(serde_json::to_vec(&json!({
Expand Down Expand Up @@ -4195,8 +4196,10 @@ async fn routing_log_prefers_canonical_and_preserves_legacy_fallback() -> TestRe
let first: Value =
serde_json::from_str(records.lines().next().ok_or("routing log was empty")?)?;
assert_eq!(first["session_id"], "canonical-session");
assert_eq!(first["turn_id"], "canonical-turn");
assert_eq!(first["origin"], r#"custom-agent/"quoted"\path"#);
let second: Value = serde_json::from_str(records.lines().nth(1).ok_or("missing record")?)?;
assert_eq!(second["turn_id"], Value::Null);
assert_eq!(second.get("origin"), Some(&Value::Null));
assert!(
first["ts"]
Expand Down Expand Up @@ -4304,6 +4307,7 @@ async fn routing_log_keeps_the_canonical_session_id_until_a_stream_drains() -> T
})),
&[
("x-switchyard-session-id", "streaming-session"),
("x-switchyard-turn-id", "streaming-turn"),
("x-switchyard-origin", "codex-cli"),
],
)
Expand All @@ -4328,6 +4332,7 @@ async fn routing_log_keeps_the_canonical_session_id_until_a_stream_drains() -> T

let record: Value = serde_json::from_str(&std::fs::read_to_string(log_path)?)?;
assert_eq!(record["route_id"], ROUTE_MODEL);
assert_eq!(record["turn_id"], "streaming-turn");
assert_eq!(record["algorithm"], "random");
assert_eq!(record["origin"], "codex-cli");
Ok(())
Expand Down
Loading