Skip to main content

synapse/routing/
request.rs

1//! OpenAI-compatible chat request body + native extension block.
2
3use 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, // string, array of parts, or null on a tool-call turn
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub tool_calls: Option<Vec<Value>>, // assistant turn: [{id,type,function:{name,arguments}}]
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub tool_call_id: Option<String>, // role:"tool" result correlation
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub name: Option<String>, // optional name (tool result or system message)
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct ResponseFormat {
45    #[serde(rename = "type")]
46    pub kind: String, // "text" | "json_object" | "json_schema"
47    #[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}