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