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