systemprompt_models/wire/canonical/
request.rs1use crate::gateway_hash::conversation_prefix_hash;
7use serde_json::Value;
8use systemprompt_identifiers::GatewayConversationId;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Role {
12 System,
13 User,
14 Assistant,
15 Tool,
16}
17
18impl Role {
19 pub const fn as_str(self) -> &'static str {
20 match self {
21 Self::System => "system",
22 Self::User => "user",
23 Self::Assistant => "assistant",
24 Self::Tool => "tool",
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ImageDetail {
31 Auto,
32 Low,
33 High,
34}
35
36impl ImageDetail {
37 pub const fn as_str(self) -> &'static str {
38 match self {
39 Self::Auto => "auto",
40 Self::Low => "low",
41 Self::High => "high",
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
47pub enum ImageSource {
48 Base64 {
49 media_type: String,
50 data: String,
51 detail: Option<ImageDetail>,
52 },
53 Url {
54 url: String,
55 detail: Option<ImageDetail>,
56 },
57}
58
59#[derive(Debug, Clone)]
60pub enum CanonicalContent {
61 Text(String),
62 Image(ImageSource),
63 ToolUse {
64 id: String,
65 name: String,
66 input: Value,
67 },
68 ToolResult {
69 tool_use_id: String,
70 content: Vec<Self>,
71 is_error: bool,
72 },
73 Thinking {
74 text: String,
75 signature: Option<String>,
76 },
77}
78
79#[derive(Debug, Clone)]
80pub struct CanonicalMessage {
81 pub role: Role,
82 pub content: Vec<CanonicalContent>,
83}
84
85#[derive(Debug, Clone)]
86pub struct CanonicalTool {
87 pub name: String,
88 pub description: Option<String>,
89 pub input_schema: Value,
90}
91
92#[derive(Debug, Clone)]
93pub enum CanonicalToolChoice {
94 Auto,
95 Any,
96 None,
97 Required,
98 Tool(String),
99}
100
101#[derive(Debug, Clone, Copy, Default)]
102pub struct ThinkingConfig {
103 pub enabled: bool,
104 pub budget_tokens: Option<u32>,
105}
106
107#[derive(Debug, Clone)]
108pub enum ResponseFormat {
109 JsonObject,
110 JsonSchema {
111 name: String,
112 schema: Value,
113 strict: bool,
114 },
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum ReasoningEffort {
119 Low,
120 Medium,
121 High,
122}
123
124impl ReasoningEffort {
125 pub const fn as_str(self) -> &'static str {
126 match self {
127 Self::Low => "low",
128 Self::Medium => "medium",
129 Self::High => "high",
130 }
131 }
132}
133
134#[derive(Debug, Clone, Default)]
135pub struct SearchConfig {
136 pub max_uses: Option<u32>,
137 pub context_size: Option<String>,
138 pub urls: Vec<String>,
139}
140
141#[derive(Debug, Clone)]
142pub struct CanonicalRequest {
143 pub model: String,
144 pub system: Option<String>,
145 pub messages: Vec<CanonicalMessage>,
146 pub max_tokens: u32,
147 pub temperature: Option<f32>,
148 pub top_p: Option<f32>,
149 pub top_k: Option<i32>,
150 pub stop_sequences: Vec<String>,
151 pub tools: Vec<CanonicalTool>,
152 pub tool_choice: Option<CanonicalToolChoice>,
153 pub stream: bool,
154 pub thinking: Option<ThinkingConfig>,
155 pub metadata: Option<Value>,
156 pub response_format: Option<ResponseFormat>,
157 pub reasoning_effort: Option<ReasoningEffort>,
158 pub search: Option<SearchConfig>,
159 pub code_execution: bool,
160 pub presence_penalty: Option<f32>,
161 pub frequency_penalty: Option<f32>,
162}
163
164impl CanonicalRequest {
165 pub fn flatten_text(&self) -> String {
166 let mut out = String::new();
167 if let Some(sys) = &self.system {
168 push_with_sep(&mut out, sys);
169 }
170 for msg in &self.messages {
171 for part in &msg.content {
172 flatten_part(&mut out, part);
173 }
174 }
175 out
176 }
177
178 pub fn derived_gateway_conversation_id(&self) -> Option<GatewayConversationId> {
179 let first = self.messages.first()?;
180 let mut content = String::new();
181 for part in &first.content {
182 flatten_part(&mut content, part);
183 }
184 let hash = conversation_prefix_hash(self.system.as_deref(), first.role.as_str(), &content);
185 Some(GatewayConversationId::from_prefix_hash(hash))
186 }
187
188 pub fn flatten_message_text(&self, role: Role) -> Option<String> {
189 let mut out = String::new();
190 for msg in &self.messages {
191 if msg.role != role {
192 continue;
193 }
194 for part in &msg.content {
195 flatten_part(&mut out, part);
196 }
197 }
198 if out.is_empty() { None } else { Some(out) }
199 }
200}
201
202fn flatten_part(out: &mut String, part: &CanonicalContent) {
203 match part {
204 CanonicalContent::Text(t) => push_with_sep(out, t),
205 CanonicalContent::Thinking { text, .. } => push_with_sep(out, text),
206 CanonicalContent::ToolUse { name, input, .. } => {
207 push_with_sep(out, &format!("[tool_use:{name} {input}]"));
208 },
209 CanonicalContent::ToolResult { content, .. } => {
210 for inner in content {
211 flatten_part(out, inner);
212 }
213 },
214 CanonicalContent::Image(_) => {},
215 }
216}
217
218fn push_with_sep(out: &mut String, fragment: &str) {
219 if fragment.is_empty() {
220 return;
221 }
222 if !out.is_empty() {
223 out.push('\n');
224 }
225 out.push_str(fragment);
226}