Skip to main content

systemprompt_models/ai/
request.rs

1//! Provider-agnostic inference request types.
2//!
3//! [`AiRequest`] is the unified request shape passed to any LLM provider: a
4//! sequence of [`AiMessage`]s (each optionally carrying multimodal
5//! [`AiContentPart`]s), the provider/model config, sampling params, available
6//! tools, and structured-output options. Build via [`AiRequestBuilder`] for the
7//! optional fields.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use super::response_format::StructuredOutputOptions;
13use super::sampling::{ProviderConfig, SamplingParams};
14use super::tools::McpTool;
15use crate::execution::context::RequestContext;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19#[serde(tag = "type", rename_all = "snake_case")]
20pub enum AiContentPart {
21    Text { text: String },
22    Image { mime_type: String, data: String },
23    Audio { mime_type: String, data: String },
24    Video { mime_type: String, data: String },
25}
26
27impl AiContentPart {
28    pub fn text(text: impl Into<String>) -> Self {
29        Self::Text { text: text.into() }
30    }
31
32    pub fn image(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
33        Self::Image {
34            mime_type: mime_type.into(),
35            data: data.into(),
36        }
37    }
38
39    pub fn audio(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
40        Self::Audio {
41            mime_type: mime_type.into(),
42            data: data.into(),
43        }
44    }
45
46    pub fn video(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
47        Self::Video {
48            mime_type: mime_type.into(),
49            data: data.into(),
50        }
51    }
52
53    pub const fn is_media(&self) -> bool {
54        matches!(
55            self,
56            Self::Image { .. } | Self::Audio { .. } | Self::Video { .. }
57        )
58    }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct AiMessage {
63    pub role: MessageRole,
64    pub content: String,
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub parts: Vec<AiContentPart>,
67}
68
69impl AiMessage {
70    pub fn user(content: impl Into<String>) -> Self {
71        Self {
72            role: MessageRole::User,
73            content: content.into(),
74            parts: Vec::new(),
75        }
76    }
77
78    pub fn assistant(content: impl Into<String>) -> Self {
79        Self {
80            role: MessageRole::Assistant,
81            content: content.into(),
82            parts: Vec::new(),
83        }
84    }
85
86    pub fn system(content: impl Into<String>) -> Self {
87        Self {
88            role: MessageRole::System,
89            content: content.into(),
90            parts: Vec::new(),
91        }
92    }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "lowercase")]
97pub enum MessageRole {
98    System,
99    User,
100    Assistant,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct AiRequest {
105    pub messages: Vec<AiMessage>,
106    pub provider_config: ProviderConfig,
107    pub context: RequestContext,
108    pub sampling: Option<SamplingParams>,
109    pub tools: Option<Vec<McpTool>>,
110    pub structured_output: Option<StructuredOutputOptions>,
111    pub system_prompt: Option<String>,
112}
113
114impl AiRequest {
115    pub fn builder(
116        messages: Vec<AiMessage>,
117        provider: impl Into<String>,
118        model: impl Into<String>,
119        max_output_tokens: u32,
120        context: RequestContext,
121    ) -> AiRequestBuilder {
122        AiRequestBuilder::new(messages, provider, model, max_output_tokens, context)
123    }
124
125    pub fn has_tools(&self) -> bool {
126        self.tools.as_ref().is_some_and(|t| !t.is_empty())
127    }
128
129    pub fn provider(&self) -> &str {
130        &self.provider_config.provider
131    }
132
133    pub fn model(&self) -> &str {
134        &self.provider_config.model
135    }
136
137    pub const fn max_output_tokens(&self) -> u32 {
138        self.provider_config.max_output_tokens
139    }
140}
141
142#[derive(Debug)]
143pub struct AiRequestBuilder {
144    messages: Vec<AiMessage>,
145    provider_config: ProviderConfig,
146    context: RequestContext,
147    sampling: Option<SamplingParams>,
148    tools: Option<Vec<McpTool>>,
149    structured_output: Option<StructuredOutputOptions>,
150    system_prompt: Option<String>,
151}
152
153impl AiRequestBuilder {
154    pub fn new(
155        messages: Vec<AiMessage>,
156        provider: impl Into<String>,
157        model: impl Into<String>,
158        max_output_tokens: u32,
159        context: RequestContext,
160    ) -> Self {
161        Self {
162            messages,
163            provider_config: ProviderConfig::new(provider, model, max_output_tokens),
164            context,
165            sampling: None,
166            tools: None,
167            structured_output: None,
168            system_prompt: None,
169        }
170    }
171
172    pub fn with_sampling(mut self, sampling: SamplingParams) -> Self {
173        self.sampling = Some(sampling);
174        self
175    }
176
177    pub fn with_tools(mut self, tools: Vec<McpTool>) -> Self {
178        self.tools = Some(tools);
179        self
180    }
181
182    pub fn with_structured_output(mut self, options: StructuredOutputOptions) -> Self {
183        self.structured_output = Some(options);
184        self
185    }
186
187    pub fn build(self) -> AiRequest {
188        AiRequest {
189            messages: self.messages,
190            provider_config: self.provider_config,
191            context: self.context,
192            sampling: self.sampling,
193            tools: self.tools,
194            structured_output: self.structured_output,
195            system_prompt: self.system_prompt,
196        }
197    }
198}