Skip to main content

switchyard_libsy/algorithms/
passthrough.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Single-target routing for direct model calls and integration diagnostics.
5
6use std::sync::Arc;
7
8use switchyard_protocol::{Request, Response};
9
10use crate::Result;
11use crate::core::algorithm::{Algorithm, Driver, LlmTarget};
12use switchyard_protocol::{Context, Decision, RoutedLlmClient};
13
14/// Routing algorithm that always calls one configured target.
15pub struct Passthrough {
16    target: LlmTarget,
17}
18
19impl Passthrough {
20    /// Creates an algorithm that always calls `target`.
21    pub fn new(target: LlmTarget) -> Self {
22        Passthrough { target }
23    }
24}
25
26/// Decision emitted before [`Passthrough`] calls its configured target.
27pub(crate) struct PassthroughDecision {
28    model_id: String,
29}
30
31impl Decision for PassthroughDecision {
32    fn selected_model(&self) -> &str {
33        &self.model_id
34    }
35    fn reasoning(&self) -> Option<&str> {
36        // There is no decision to take, so no reasoning
37        None
38    }
39    fn as_any(&self) -> &dyn std::any::Any {
40        self
41    }
42}
43
44#[async_trait::async_trait]
45impl Algorithm for Passthrough {
46    fn name(&self) -> &str {
47        "passthrough"
48    }
49
50    fn count_tokens_client(&self) -> Option<Arc<dyn RoutedLlmClient>> {
51        self.target
52            .llm_client
53            .as_ref()
54            .filter(|client| client.supports_count_tokens())
55            .cloned()
56    }
57
58    async fn create_run_task(
59        self: Arc<Self>,
60        ctx: Context,
61        driver: Driver,
62        request: Request,
63    ) -> Result<Response> {
64        let decision: Arc<dyn Decision> = Arc::new(PassthroughDecision {
65            model_id: self.target.semantic_name.clone(),
66        });
67        driver.info(ctx.clone(), decision.clone()).await?;
68        driver
69            .call_llm_target(ctx, &self.target, request, decision)
70            .await
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use std::sync::Arc;
77
78    use super::Passthrough;
79    use crate::core::algorithm::{Algorithm, LlmTarget};
80    use crate::text::{completion_text, text_request, text_response};
81    use switchyard_protocol::{Context, Decision, LlmResponse, Request, Response, RoutedLlmClient};
82
83    /// Echoes the selected target so tests can inspect which target was called.
84    /// TODO: Duplicated from rand.rs
85    struct EchoClient;
86
87    #[async_trait::async_trait]
88    impl RoutedLlmClient for EchoClient {
89        async fn call(
90            &self,
91            _ctx: Context,
92            _request: Request,
93            decision: Arc<dyn Decision>,
94        ) -> std::result::Result<Response, switchyard_protocol::LlmClientError> {
95            Ok(Response {
96                llm_response: LlmResponse::Agg(text_response(None, decision.selected_model())),
97                metadata: None,
98            })
99        }
100    }
101
102    #[tokio::test]
103    async fn test_passthrough() -> crate::Result<()> {
104        const MODEL_ID: &str = "testing/passthrough";
105        let request = Request {
106            llm_request: text_request(Some("auto".to_string()), "hi"),
107            raw_request: None,
108            metadata: None,
109        };
110        let algorithm: Arc<dyn Algorithm> = Arc::new(Passthrough::new(LlmTarget {
111            semantic_name: MODEL_ID.to_string(),
112            llm_client: Some(Arc::new(EchoClient)),
113        }));
114        let (trace, response) = algorithm.run(Context::default(), request).await?;
115
116        assert_eq!(
117            response
118                .llm_response
119                .as_agg()
120                .map(completion_text)
121                .unwrap_or_default(),
122            MODEL_ID
123        );
124        assert_eq!(trace.len(), 1);
125        assert_eq!(trace[0].selected_model(), MODEL_ID);
126        Ok(())
127    }
128}