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 structured_content: Option<Value>,
73 meta: Option<Value>,
74 },
75 Thinking {
76 text: String,
77 signature: Option<String>,
78 },
79}
80
81#[derive(Debug, Clone)]
82pub struct CanonicalMessage {
83 pub role: Role,
84 pub content: Vec<CanonicalContent>,
85}
86
87#[derive(Debug, Clone)]
88pub struct CanonicalTool {
89 pub name: String,
90 pub description: Option<String>,
91 pub input_schema: Value,
92}
93
94#[derive(Debug, Clone)]
95pub enum CanonicalToolChoice {
96 Auto,
97 Any,
98 None,
99 Required,
100 Tool(String),
101}
102
103#[derive(Debug, Clone, Copy, Default)]
104pub struct ThinkingConfig {
105 pub enabled: bool,
106 pub budget_tokens: Option<u32>,
107}
108
109#[derive(Debug, Clone)]
110pub enum ResponseFormat {
111 JsonObject,
112 JsonSchema {
113 name: String,
114 schema: Value,
115 strict: bool,
116 },
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ReasoningEffort {
121 Low,
122 Medium,
123 High,
124}
125
126impl ReasoningEffort {
127 pub const fn as_str(self) -> &'static str {
128 match self {
129 Self::Low => "low",
130 Self::Medium => "medium",
131 Self::High => "high",
132 }
133 }
134}
135
136#[derive(Debug, Clone, Default)]
137pub struct SearchConfig {
138 pub max_uses: Option<u32>,
139 pub context_size: Option<String>,
140 pub urls: Vec<String>,
141}
142
143#[derive(Debug, Clone)]
144pub struct CanonicalRequest {
145 pub model: String,
146 pub system: Option<String>,
147 pub messages: Vec<CanonicalMessage>,
148 pub max_tokens: u32,
149 pub temperature: Option<f32>,
150 pub top_p: Option<f32>,
151 pub top_k: Option<i32>,
152 pub stop_sequences: Vec<String>,
153 pub tools: Vec<CanonicalTool>,
154 pub tool_choice: Option<CanonicalToolChoice>,
155 pub stream: bool,
156 pub thinking: Option<ThinkingConfig>,
157 pub metadata: Option<Value>,
158 pub response_format: Option<ResponseFormat>,
159 pub reasoning_effort: Option<ReasoningEffort>,
160 pub search: Option<SearchConfig>,
161 pub code_execution: bool,
162 pub presence_penalty: Option<f32>,
163 pub frequency_penalty: Option<f32>,
164}
165
166impl CanonicalRequest {
167 pub fn flatten_text(&self) -> String {
168 let mut out = String::new();
169 if let Some(sys) = &self.system {
170 push_with_sep(&mut out, sys);
171 }
172 for msg in &self.messages {
173 for part in &msg.content {
174 flatten_part(&mut out, part);
175 }
176 }
177 out
178 }
179
180 pub fn derived_gateway_conversation_id(&self) -> Option<GatewayConversationId> {
181 let first = self.messages.first()?;
182 let mut content = String::new();
183 for part in &first.content {
184 flatten_part(&mut content, part);
185 }
186 let hash = conversation_prefix_hash(self.system.as_deref(), first.role.as_str(), &content);
187 Some(GatewayConversationId::from_prefix_hash(hash))
188 }
189
190 pub fn flatten_message_text(&self, role: Role) -> Option<String> {
191 let mut out = String::new();
192 for msg in &self.messages {
193 if msg.role != role {
194 continue;
195 }
196 for part in &msg.content {
197 flatten_part(&mut out, part);
198 }
199 }
200 if out.is_empty() { None } else { Some(out) }
201 }
202}
203
204fn flatten_part(out: &mut String, part: &CanonicalContent) {
205 match part {
206 CanonicalContent::Text(t) => push_with_sep(out, t),
207 CanonicalContent::Thinking { text, .. } => push_with_sep(out, text),
208 CanonicalContent::ToolUse { name, input, .. } => {
209 push_with_sep(out, &format!("[tool_use:{name} {input}]"));
210 },
211 CanonicalContent::ToolResult { content, .. } => {
212 for inner in content {
213 flatten_part(out, inner);
214 }
215 },
216 CanonicalContent::Image(_) => {},
217 }
218}
219
220fn push_with_sep(out: &mut String, fragment: &str) {
221 if fragment.is_empty() {
222 return;
223 }
224 if !out.is_empty() {
225 out.push('\n');
226 }
227 out.push_str(fragment);
228}