systemprompt_api/services/gateway/protocol/
canonical.rs1use serde_json::Value;
2use systemprompt_identifiers::GatewayConversationId;
3use systemprompt_models::gateway_hash::conversation_prefix_hash;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Role {
7 System,
8 User,
9 Assistant,
10 Tool,
11}
12
13impl Role {
14 pub const fn as_str(self) -> &'static str {
15 match self {
16 Self::System => "system",
17 Self::User => "user",
18 Self::Assistant => "assistant",
19 Self::Tool => "tool",
20 }
21 }
22}
23
24#[derive(Debug, Clone)]
25pub enum ImageSource {
26 Base64 { media_type: String, data: String },
27 Url(String),
28}
29
30#[derive(Debug, Clone)]
31pub enum CanonicalContent {
32 Text(String),
33 Image(ImageSource),
34 ToolUse {
35 id: String,
36 name: String,
37 input: Value,
38 },
39 ToolResult {
40 tool_use_id: String,
41 content: Vec<Self>,
42 is_error: bool,
43 },
44 Thinking {
45 text: String,
46 signature: Option<String>,
47 },
48}
49
50#[derive(Debug, Clone)]
51pub struct CanonicalMessage {
52 pub role: Role,
53 pub content: Vec<CanonicalContent>,
54}
55
56#[derive(Debug, Clone)]
57pub struct CanonicalTool {
58 pub name: String,
59 pub description: Option<String>,
60 pub input_schema: Value,
61}
62
63#[derive(Debug, Clone)]
64pub enum CanonicalToolChoice {
65 Auto,
66 Any,
67 None,
68 Required,
69 Tool(String),
70}
71
72#[derive(Debug, Clone, Copy, Default)]
73pub struct ThinkingConfig {
74 pub enabled: bool,
75 pub budget_tokens: Option<u32>,
76}
77
78#[derive(Debug, Clone)]
79pub struct CanonicalRequest {
80 pub model: String,
81 pub system: Option<String>,
82 pub messages: Vec<CanonicalMessage>,
83 pub max_tokens: u32,
84 pub temperature: Option<f32>,
85 pub top_p: Option<f32>,
86 pub top_k: Option<i32>,
87 pub stop_sequences: Vec<String>,
88 pub tools: Vec<CanonicalTool>,
89 pub tool_choice: Option<CanonicalToolChoice>,
90 pub stream: bool,
91 pub thinking: Option<ThinkingConfig>,
92 pub metadata: Option<Value>,
93}
94
95impl CanonicalRequest {
96 pub fn flatten_text(&self) -> String {
97 let mut out = String::new();
98 if let Some(sys) = &self.system {
99 push_with_sep(&mut out, sys);
100 }
101 for msg in &self.messages {
102 for part in &msg.content {
103 flatten_part(&mut out, part);
104 }
105 }
106 out
107 }
108
109 pub fn derived_gateway_conversation_id(&self) -> Option<GatewayConversationId> {
114 let first = self.messages.first()?;
115 let mut content = String::new();
116 for part in &first.content {
117 flatten_part(&mut content, part);
118 }
119 let hash = conversation_prefix_hash(self.system.as_deref(), first.role.as_str(), &content);
120 Some(GatewayConversationId::from_prefix_hash(hash))
121 }
122
123 pub fn flatten_message_text(&self, role: Role) -> Option<String> {
124 let mut out = String::new();
125 for msg in &self.messages {
126 if msg.role != role {
127 continue;
128 }
129 for part in &msg.content {
130 flatten_part(&mut out, part);
131 }
132 }
133 if out.is_empty() { None } else { Some(out) }
134 }
135}
136
137fn flatten_part(out: &mut String, part: &CanonicalContent) {
138 match part {
139 CanonicalContent::Text(t) => push_with_sep(out, t),
140 CanonicalContent::Thinking { text, .. } => push_with_sep(out, text),
141 CanonicalContent::ToolUse { name, input, .. } => {
142 push_with_sep(out, &format!("[tool_use:{name} {input}]"));
143 },
144 CanonicalContent::ToolResult { content, .. } => {
145 for inner in content {
146 flatten_part(out, inner);
147 }
148 },
149 CanonicalContent::Image(_) => {},
150 }
151}
152
153fn push_with_sep(out: &mut String, fragment: &str) {
154 if fragment.is_empty() {
155 return;
156 }
157 if !out.is_empty() {
158 out.push('\n');
159 }
160 out.push_str(fragment);
161}