synapse/routing/
request.rs1use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct ChatRequest {
8 pub model: String,
9 pub messages: Vec<Message>,
10 #[serde(default)]
11 pub temperature: Option<f32>,
12 #[serde(default)]
13 pub max_tokens: Option<u32>,
14 #[serde(default)]
15 pub stream: Option<bool>,
16 #[serde(default)]
17 pub response_format: Option<ResponseFormat>,
18 #[serde(default)]
19 pub routing_strategy: Option<String>,
20 #[serde(default)]
21 pub vertex: Option<VertexExt>,
22 #[serde(default)]
23 pub tools: Option<Vec<Value>>,
24 #[serde(default)]
25 pub tool_choice: Option<Value>,
26 #[serde(flatten, default)]
27 pub passthrough: Map<String, Value>,
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct Message {
32 pub role: String,
33 #[serde(default, skip_serializing_if = "Value::is_null")]
34 pub content: Value, #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub tool_calls: Option<Vec<Value>>, #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub tool_call_id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub name: Option<String>, }
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct ResponseFormat {
45 #[serde(rename = "type")]
46 pub kind: String, #[serde(default)]
48 pub json_schema: Option<Value>,
49}
50
51#[derive(Debug, Clone, Default, Deserialize, Serialize)]
52pub struct VertexExt {
53 #[serde(default)]
54 pub cached_content: Option<String>,
55 #[serde(default)]
56 pub media_uris: Option<Vec<String>>,
57 #[serde(default)]
58 pub response_schema: Option<Value>,
59 #[serde(default)]
63 pub thinking_config: Option<Value>,
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn parses_plain_openai_body() {
72 let body = serde_json::json!({
73 "model": "gemini-pro",
74 "messages": [{"role": "user", "content": "hi"}],
75 "temperature": 0.2
76 });
77 let req: ChatRequest = serde_json::from_value(body).unwrap();
78 assert_eq!(req.model, "gemini-pro");
79 assert_eq!(req.messages.len(), 1);
80 assert!(req.vertex.is_none());
81 assert!(req.passthrough.is_empty());
82 }
83
84 #[test]
85 fn captures_vertex_extension_and_passthrough() {
86 let body = serde_json::json!({
87 "model": "gemini-pro",
88 "messages": [{"role": "user", "content": "hi"}],
89 "top_k": 40,
90 "vertex": { "cached_content": "cachedContents/abc" }
91 });
92 let req: ChatRequest = serde_json::from_value(body).unwrap();
93 assert_eq!(
94 req.vertex.unwrap().cached_content.as_deref(),
95 Some("cachedContents/abc")
96 );
97 assert_eq!(req.passthrough.get("top_k"), Some(&serde_json::json!(40)));
98 }
99
100 #[test]
101 fn captures_vertex_thinking_config() {
102 let body = serde_json::json!({
103 "model": "gemini-3-pro",
104 "messages": [{"role": "user", "content": "hi"}],
105 "vertex": { "thinking_config": { "thinkingLevel": "low" } }
106 });
107 let req: ChatRequest = serde_json::from_value(body).unwrap();
108 assert_eq!(
109 req.vertex.unwrap().thinking_config,
110 Some(serde_json::json!({ "thinkingLevel": "low" }))
111 );
112 }
113
114 #[test]
115 fn parses_tools_and_tool_messages() {
116 let body = serde_json::json!({
117 "model": "gemini-pro",
118 "messages": [
119 {"role": "user", "content": "weather in SF?"},
120 {"role": "assistant", "content": null,
121 "tool_calls": [{"id": "call_0", "type": "function",
122 "function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}}]},
123 {"role": "tool", "tool_call_id": "call_0", "content": "21C"}
124 ],
125 "tools": [{"type": "function", "function": {"name": "get_weather",
126 "description": "Lookup", "parameters": {"type": "object"}}}],
127 "tool_choice": "auto"
128 });
129 let req: ChatRequest = serde_json::from_value(body).unwrap();
130 assert_eq!(req.tools.as_ref().unwrap().len(), 1);
131 assert_eq!(req.tool_choice, Some(serde_json::json!("auto")));
132 let asst = &req.messages[1];
133 assert!(asst.tool_calls.is_some());
134 let tool = &req.messages[2];
135 assert_eq!(tool.tool_call_id.as_deref(), Some("call_0"));
136 }
137}