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
5 changes: 4 additions & 1 deletion crates/libsy-llm-client/src/observation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use std::sync::Arc;
use std::time::Duration;

use switchyard_libsy::OutcomeMetadata;
use switchyard_protocol::{ModelId, Usage};

/// One completed model call observed while serving an algorithm run.
Expand All @@ -21,9 +22,11 @@ pub struct LlmCallObservation {
pub usage: Option<Usage>,
}

/// One request-scoped observation emitted by the algorithm runner.
/// Events emitted inline while [`crate::run`] serves a routing request.
#[derive(Clone, Debug)]
pub enum RunObservation {
Comment thread
afourniernv marked this conversation as resolved.
/// Metadata attached to the completed routing outcome.
Outcome(OutcomeMetadata),
/// A completed model call requested by the algorithm for routing work.
LlmCall(LlmCallObservation),
/// A completed terminal model call made from the routing outcome.
Expand Down
14 changes: 12 additions & 2 deletions crates/libsy-llm-client/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ use crate::{metrics, observability};
/// Run one request to completion, serving every offloaded model call with `client`.
///
/// Returns the model selected by the algorithm and the final [`Response`]. `observer`, when
/// present, receives each completed routing or answer call and the routing overhead.
/// present, receives metadata for the routing outcome, each completed routing or answer call,
/// and the routing overhead.
///
/// `clients` resolves each offloaded call to the client for the target the algorithm
/// selected — an algorithm may route among targets served by different providers, so this is
Expand Down Expand Up @@ -71,6 +72,11 @@ pub async fn run(
metrics::record_routing_overhead(&algorithm_name, overhead);

let selected_model_id = outcome.selected_model_id()?.clone();
if let Some(observer) = &observer
&& let Some(metadata) = outcome.metadata
{
observer(RunObservation::Outcome(metadata));
}
let (result, answer_duration) = if let Some(response) = outcome.response {
(Ok(response), None)
} else {
Expand Down Expand Up @@ -661,9 +667,13 @@ mod tests {
assert!(matches!(observations[0], RunObservation::AnswerCall(_)));
assert!(matches!(
observations[1],
RunObservation::Outcome(ref metadata) if metadata.algorithm == "answered_test"
));
assert!(matches!(
observations[2],
RunObservation::RoutingOverhead(_)
));
assert_eq!(observations.len(), 2);
assert_eq!(observations.len(), 3);
Ok(())
}

Expand Down
11 changes: 8 additions & 3 deletions crates/libsy-llm-client/tests/observability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1218,15 +1218,20 @@ async fn observed_run_reports_one_successful_routed_call() -> switchyard_libsy::
Some(Some(MODEL))
);
let observations = observations.lock();
assert_eq!(observations.len(), 2);
let RunObservation::AnswerCall(observation) = &observations[0] else {
// Outcome metadata precedes the answer call and final overhead observation.
assert_eq!(observations.len(), 3);
let RunObservation::Outcome(metadata) = &observations[0] else {
return Err(test_error("expected an outcome observation"));
};
assert_eq!(metadata.algorithm, ALGO);
let RunObservation::AnswerCall(observation) = &observations[1] else {
return Err(test_error("expected an answer-call observation"));
};
assert_eq!(observation.selected_model, MODEL);
assert!(observation.is_success);
assert!(observation.usage.is_some());
assert!(matches!(
observations[1],
observations[2],
RunObservation::RoutingOverhead(_)
));
Ok(())
Expand Down
8 changes: 6 additions & 2 deletions crates/switchyard-nemo-relay-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,14 @@ meaning requires a new schema version.
| `switchyard.routing.requested` | `algorithm` |
| `switchyard.routing.llm_call` | `call_index`, `selected_model`, `call_role`, `outcome`, `latency_ms` |
| `switchyard.routing.overhead` | `latency_ms` |
| `switchyard.routing.decision` | `algorithm`, `selected_model`, nullable `served_model`, nullable `fallback_used` |
| `switchyard.routing.error` | `failure_kind`; optional `category`, `phase`, `upstream_status`, and `target` |
| `switchyard.routing.decision` | `algorithm`, optional `outcome_id`, `selected_model`, nullable `served_model`, nullable `fallback_used`, and optional `evidence` |
| `switchyard.routing.error` | `failure_kind`; route-execution failures also include `category`, `phase`, nullable `upstream_status` and `target`, and may include `outcome_id` and `evidence` |

`served_model` and `fallback_used` are `null` when serving metadata is unavailable.
`outcome_id` is present when the algorithm runner supplies outcome metadata.
`evidence` is an object containing the supported string fields `source`, `verdict`,
`trigger`, and `reason_code`, and numeric fields `score`, `confidence`, and `threshold`.
String values longer than 64 bytes are omitted and should be stable, non-sensitive labels.

## Failure policy

Expand Down
Loading
Loading