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