Skip to main content

switchyard_protocol/
llm.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Provider-neutral conversation types shared by routing, clients, and translation.
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::format::FormatId;
12
13/// Actor role normalized across provider APIs.
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
15pub enum Role {
16    System,
17    Developer,
18    User,
19    Assistant,
20    Tool,
21}
22
23/// Instruction content separated from normal conversation messages.
24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
25pub struct InstructionBlock {
26    pub role: Role,
27    pub content: Vec<ContentBlock>,
28}
29
30/// One normalized conversation message.
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
32pub struct Message {
33    pub role: Role,
34    pub content: Vec<ContentBlock>,
35}
36
37impl Message {
38    /// Creates a text-only message for the given role.
39    pub fn text(role: Role, text: impl Into<String>) -> Self {
40        Self {
41            role,
42            content: vec![ContentBlock::Text { text: text.into() }],
43        }
44    }
45
46    /// Concatenates text-like content blocks when the message has any.
47    pub fn text_content(&self, separator: &str) -> Option<String> {
48        let parts = self
49            .content
50            .iter()
51            .filter_map(|block| match block {
52                ContentBlock::Text { text } => Some(text.as_str()),
53                ContentBlock::Refusal { text } => Some(text.as_str()),
54                _ => None,
55            })
56            .collect::<Vec<_>>();
57        if parts.is_empty() {
58            None
59        } else {
60            Some(parts.join(separator))
61        }
62    }
63}
64
65/// Normalized content block variants carried by messages and tool results.
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67pub enum ContentBlock {
68    Text {
69        text: String,
70    },
71    Reasoning {
72        text: String,
73        signature: Option<String>,
74    },
75    Image {
76        source: ImageSource,
77    },
78    Audio {
79        source: MediaSource,
80    },
81    Video {
82        source: MediaSource,
83    },
84    File {
85        source: FileSource,
86    },
87    ToolCall(ToolCall),
88    ToolResult(ToolResult),
89    Refusal {
90        text: String,
91    },
92    Unknown {
93        provider: FormatId,
94        raw: Value,
95    },
96}
97
98/// Image payload forms supported by the conversation model.
99#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100pub enum ImageSource {
101    Url {
102        url: String,
103        detail: Option<String>,
104    },
105    Base64 {
106        media_type: Option<String>,
107        data: String,
108    },
109    Raw(Value),
110}
111
112/// File payload forms supported by the conversation model.
113#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
114pub enum FileSource {
115    FileId(String),
116    FileData {
117        data: String,
118        filename: Option<String>,
119    },
120    Raw(Value),
121}
122
123/// Audio and video payload forms supported by the conversation model.
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
125pub enum MediaSource {
126    Url {
127        url: String,
128        media_type: Option<String>,
129    },
130    Base64 {
131        media_type: Option<String>,
132        data: String,
133    },
134    Raw(Value),
135}
136
137/// Normalized assistant tool call.
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139pub struct ToolCall {
140    pub id: String,
141    pub name: String,
142    pub arguments: Value,
143}
144
145/// Normalized tool result message content.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147pub struct ToolResult {
148    pub tool_call_id: String,
149    pub content: Vec<ContentBlock>,
150    pub is_error: Option<bool>,
151}
152
153/// Normalized tool definition.
154#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
155pub struct ToolDefinition {
156    pub name: String,
157    pub description: Option<String>,
158    pub parameters: Value,
159    pub strict: Option<bool>,
160}
161
162/// Normalized tool choice policy.
163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
164pub enum ToolChoice {
165    Auto,
166    Required,
167    None,
168    Tool { name: String },
169    Raw(Value),
170}
171
172/// Provider sampling parameters with common cross-provider names.
173#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
174pub struct SamplingParams {
175    pub temperature: Option<f64>,
176    pub top_p: Option<f64>,
177    pub top_k: Option<i64>,
178}
179
180/// Output budget and structured-output options.
181#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
182pub struct OutputParams {
183    pub max_output_tokens: Option<u64>,
184    pub response_format: Option<Value>,
185}
186
187/// Provider reasoning controls preserved by translation.
188#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
189pub struct ReasoningParams {
190    pub effort: Option<String>,
191    pub raw: Option<Value>,
192}
193
194/// Provider-specific fields that do not have first-class conversation fields.
195#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
196pub struct ProviderExtensions {
197    pub fields: Map<String, Value>,
198}
199
200/// Exact source payloads retained for lossless round trips.
201#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
202pub struct PreservationMetadata {
203    pub requests: BTreeMap<FormatId, Value>,
204    pub responses: BTreeMap<FormatId, Value>,
205}
206
207/// Normalized request representation shared by Switchyard components.
208#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
209pub struct LlmRequest {
210    pub model: Option<String>,
211    pub instructions: Vec<InstructionBlock>,
212    pub messages: Vec<Message>,
213    pub tools: Vec<ToolDefinition>,
214    pub tool_choice: Option<ToolChoice>,
215    pub sampling: SamplingParams,
216    pub output: OutputParams,
217    pub reasoning: ReasoningParams,
218    pub stream: bool,
219    pub extensions: ProviderExtensions,
220    pub preservation: PreservationMetadata,
221}
222
223/// Normalized token usage counts.
224#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
225pub struct Usage {
226    pub input_tokens: Option<u64>,
227    pub output_tokens: Option<u64>,
228    pub total_tokens: Option<u64>,
229    pub reasoning_tokens: Option<u64>,
230}
231
232/// Provider-neutral event used between stream decoders and encoders.
233#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
234pub enum LlmStreamEvent {
235    MessageStart {
236        id: Option<String>,
237        model: Option<String>,
238    },
239    TextDelta {
240        index: usize,
241        text: String,
242    },
243    ReasoningDelta {
244        index: usize,
245        text: String,
246    },
247    ToolCallDelta {
248        index: usize,
249        id: Option<String>,
250        name: Option<String>,
251        arguments_delta: Option<String>,
252    },
253    Usage(Usage),
254    MessageStop {
255        reason: Option<String>,
256    },
257    Error {
258        message: String,
259    },
260}
261
262/// Normalized reason a model stopped producing output.
263#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
264pub enum StopReason {
265    EndTurn,
266    MaxTokens,
267    ToolUse,
268    ContentFilter,
269    Error,
270    Unknown,
271}
272
273/// One assistant output item in a normalized response.
274#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
275pub struct ResponseOutput {
276    pub role: Role,
277    pub content: Vec<ContentBlock>,
278    pub stop_reason: Option<StopReason>,
279}
280
281/// Normalized response representation shared by Switchyard components.
282#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
283pub struct LlmResponse {
284    pub id: Option<String>,
285    pub model: Option<String>,
286    pub outputs: Vec<ResponseOutput>,
287    pub usage: Usage,
288    pub extensions: ProviderExtensions,
289    pub preservation: PreservationMetadata,
290}
291
292impl LlmResponse {
293    /// Returns the first output item when a response has any output.
294    pub fn first_output(&self) -> Option<&ResponseOutput> {
295        self.outputs.first()
296    }
297}