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}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn parses_plain_openai_body() {
67 let body = serde_json::json!({
68 "model": "gemini-pro",
69 "messages": [{"role": "user", "content": "hi"}],
70 "temperature": 0.2
71 });
72 let req: ChatRequest = serde_json::from_value(body).unwrap();
73 assert_eq!(req.model, "gemini-pro");
74 assert_eq!(req.messages.len(), 1);
75 assert!(req.vertex.is_none());
76 assert!(req.passthrough.is_empty());
77 }
78
79 #[test]
80 fn captures_vertex_extension_and_passthrough() {
81 let body = serde_json::json!({
82 "model": "gemini-pro",
83 "messages": [{"role": "user", "content": "hi"}],
84 "top_k": 40,
85 "vertex": { "cached_content": "cachedContents/abc" }
86 });
87 let req: ChatRequest = serde_json::from_value(body).unwrap();
88 assert_eq!(
89 req.vertex.unwrap().cached_content.as_deref(),
90 Some("cachedContents/abc")
91 );
92 assert_eq!(req.passthrough.get("top_k"), Some(&serde_json::json!(40)));
93 }
94
95 #[test]
96 fn parses_tools_and_tool_messages() {
97 let body = serde_json::json!({
98 "model": "gemini-pro",
99 "messages": [
100 {"role": "user", "content": "weather in SF?"},
101 {"role": "assistant", "content": null,
102 "tool_calls": [{"id": "call_0", "type": "function",
103 "function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}}]},
104 {"role": "tool", "tool_call_id": "call_0", "content": "21C"}
105 ],
106 "tools": [{"type": "function", "function": {"name": "get_weather",
107 "description": "Lookup", "parameters": {"type": "object"}}}],
108 "tool_choice": "auto"
109 });
110 let req: ChatRequest = serde_json::from_value(body).unwrap();
111 assert_eq!(req.tools.as_ref().unwrap().len(), 1);
112 assert_eq!(req.tool_choice, Some(serde_json::json!("auto")));
113 let asst = &req.messages[1];
114 assert!(asst.tool_calls.is_some());
115 let tool = &req.messages[2];
116 assert_eq!(tool.tool_call_id.as_deref(), Some("call_0"));
117 }
118}