Skip to main content

switchyard_protocol/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4#![warn(missing_docs)]
5#![doc = include_str!("../README.md")]
6
7pub mod category;
8pub mod client;
9pub mod codex_namespaces;
10pub mod decision;
11pub mod envelope;
12pub mod format;
13pub mod infra;
14pub mod llm;
15pub mod metadata;
16pub mod model_id;
17pub mod stream;
18
19pub use category::*;
20pub use client::*;
21pub use decision::*;
22pub use envelope::*;
23pub use format::*;
24pub use infra::*;
25pub use llm::*;
26pub use metadata::*;
27pub use model_id::*;
28pub use stream::*;
29
30/// Builds a single-turn request: one user message carrying `prompt`, for `model`.
31///
32/// This deliberately creates only the common text shape. Construct [`LlmRequest`]
33/// directly for instructions, tools, multimodal content, or sampling controls.
34pub fn text_request(model: Option<String>, prompt: impl Into<String>) -> LlmRequest {
35    LlmRequest {
36        model,
37        messages: vec![Message::text(Role::User, prompt)],
38        ..LlmRequest::default()
39    }
40}
41
42/// Returns a lossy text view of all user messages, joined by newlines.
43///
44/// Only text and refusal blocks are included. Instructions, tool content,
45/// reasoning, and media are omitted. Returns an empty string when no user text exists.
46pub fn prompt_text(request: &LlmRequest) -> String {
47    request
48        .messages
49        .iter()
50        .filter(|message| message.role == Role::User)
51        .filter_map(|message| message.text_content("\n"))
52        .collect::<Vec<_>>()
53        .join("\n")
54}
55
56/// Builds a single-turn response: one assistant message carrying `completion`, for `model`.
57///
58/// Construct [`AggLlmResponse`] directly when usage, tools, reasoning, or multiple
59/// output items must be represented.
60pub fn text_response(model: Option<String>, completion: impl Into<String>) -> AggLlmResponse {
61    AggLlmResponse {
62        model,
63        outputs: vec![ResponseOutput {
64            url_citations: Vec::new(),
65            role: Role::Assistant,
66            content: vec![ContentBlock::Text {
67                text: completion.into(),
68            }],
69            stop_reason: None,
70        }],
71        ..AggLlmResponse::default()
72    }
73}
74
75/// Returns a lossy text view of the first assistant output.
76///
77/// Only text blocks from the first output are concatenated. Refusals, reasoning,
78/// tools, media, and additional outputs are omitted. Returns an empty string when
79/// no such text exists.
80pub fn completion_text(response: &AggLlmResponse) -> String {
81    response
82        .outputs
83        .first()
84        .map(|output| {
85            output
86                .content
87                .iter()
88                .filter_map(|block| match block {
89                    ContentBlock::Text { text } => Some(text.as_str()),
90                    _ => None,
91                })
92                .collect::<String>()
93        })
94        .unwrap_or_default()
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn request_round_trips_prompt_text() {
103        let req = text_request(Some("m".to_string()), "hello world");
104        assert_eq!(req.model.as_deref(), Some("m"));
105        assert_eq!(prompt_text(&req), "hello world");
106    }
107
108    #[test]
109    fn response_round_trips_completion_text() {
110        let resp = text_response(None, "the answer");
111        assert_eq!(completion_text(&resp), "the answer");
112    }
113
114    #[test]
115    fn empty_text_helpers_are_empty_strings() {
116        assert_eq!(prompt_text(&LlmRequest::default()), "");
117        assert_eq!(completion_text(&AggLlmResponse::default()), "");
118    }
119}