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