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