switchyard_protocol/envelope.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The request/response envelope: the normalized [`LlmRequest`]/[`LlmResponse`] paired
5//! with the original provider payload and correlation [`Metadata`].
6use crate::{LlmRequest, LlmResponse, Metadata, ModelId};
7use http::HeaderMap;
8
9/// A request an algorithm routes: the normalized [`LlmRequest`] plus optional
10/// host-owned raw data and correlation [`Metadata`].
11#[derive(Clone, Default)]
12pub struct Request {
13 /// The normalized request an algorithm routes.
14 pub llm_request: LlmRequest,
15 /// An optional whole request body retained by the host. libsy and the translation
16 /// codecs do not read it. Exact same-format codec replay instead uses
17 /// [`LlmRequest::preservation`].
18 pub raw_request: Option<serde_json::Value>,
19 /// Correlation metadata carried through the request.
20 pub metadata: Option<Metadata>,
21}
22
23impl Request {
24 /// Returns the model currently carried by the request, when present.
25 ///
26 /// On ingress this is the name supplied by the client. Before an offloaded model call, the
27 /// routing driver replaces it with the selected target.
28 pub fn model_id(&self) -> Option<ModelId> {
29 self.llm_request.model.as_deref().map(ModelId::from)
30 }
31}
32
33/// A response an algorithm returns: the [`LlmResponse`] (streamed or aggregate) plus
34/// optional correlation [`Metadata`].
35///
36/// Not `Clone` — `llm_response` may own a live stream.
37pub struct Response {
38 /// The neutral model response — a chunk stream or the buffered aggregate.
39 pub llm_response: LlmResponse,
40 /// Correlation metadata carried through the response.
41 pub metadata: Option<Metadata>,
42 /// Upstream HTTP response headers preserved from the LLM backend (or proxy).
43 /// Populated by the LLM client; consumers (e.g. switchyard-server) may forward
44 /// these to the downstream client for observability.
45 pub upstream_headers: HeaderMap,
46}
47
48impl Response {
49 /// Returns the model recorded by a buffered response.
50 ///
51 /// Returns `None` for a live stream because inspecting its `MessageStart`
52 /// event would require consuming the stream.
53 pub fn selected_model(&self) -> Option<&str> {
54 self.llm_response.selected_model()
55 }
56
57 /// Returns the Switchyard target that successfully served this response.
58 ///
59 /// Unlike [`Self::selected_model`], this is available for streamed responses because the
60 /// client records the target when it receives the stream handle.
61 pub fn served_model(&self) -> Option<&ModelId> {
62 self.metadata.as_ref()?.served_model.as_ref()
63 }
64
65 /// Records the Switchyard target that successfully served this response.
66 pub fn set_served_model(&mut self, model: &ModelId) {
67 self.metadata.get_or_insert_default().served_model = Some(model.clone());
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::text_response;
75
76 #[test]
77 fn served_model_round_trips_through_response_metadata() {
78 let mut response = Response {
79 llm_response: LlmResponse::Agg(text_response(None, "answer")),
80 metadata: None,
81 upstream_headers: HeaderMap::new(),
82 };
83
84 assert_eq!(response.served_model(), None);
85 response.set_served_model(&ModelId::from("first"));
86 assert_eq!(response.served_model().map(ModelId::as_str), Some("first"));
87 response.set_served_model(&ModelId::from("fallback"));
88 assert_eq!(
89 response.served_model().map(ModelId::as_str),
90 Some("fallback")
91 );
92 }
93}