Skip to main content

cognee_http_server/dto/
responses.rs

1//! DTOs for the `/api/v1/responses` router.
2//!
3//! Wire shape mirrors Python's `cognee.api.v1.responses.models`.
4//! Stage A only uses `ResponseRequestDTO` for validation (→ 400 on bad
5//! payloads before the 501 stub fires).  The response-side DTOs are shipped
6//! now so the OpenAPI document is forward-compatible with Stage B.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use utoipa::ToSchema;
12
13// ─── Request ─────────────────────────────────────────────────────────────────
14
15/// Mirrors `cognee.api.v1.responses.models.CogneeModel`.
16/// Single-variant today; kept as enum for non-breaking extensibility.
17#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, ToSchema, PartialEq, Eq)]
18pub enum CogneeModelDTO {
19    #[default]
20    #[serde(rename = "cognee-v1")]
21    CogneeV1,
22}
23
24/// Mirrors `cognee.api.v1.responses.models.ResponseRequest`.
25///
26/// Inherits `InDTO` in Python — wire is camelCase per Decision 10. Snake_case
27/// `tool_choice` and `max_completion_tokens` are accepted as inbound aliases.
28#[derive(Debug, Deserialize, ToSchema)]
29#[serde(rename_all = "camelCase")]
30pub struct ResponseRequestDTO {
31    /// Model selector. Only `"cognee-v1"` accepted today.
32    #[serde(default)]
33    pub model: CogneeModelDTO,
34    /// Natural-language input forwarded to the upstream model.
35    pub input: String,
36    /// Optional tools schema. `None` means "use server default tools".
37    pub tools: Option<Vec<ToolFunctionDTO>>,
38    /// Tool selection policy.  `"auto"` | `"none"` | `"required"` or a
39    /// JSON object `{"type":"function","function":{"name":"..."}}`.
40    /// Stored as `Value` to match Python's `Union[str, Dict[str, Any]]`.
41    #[serde(
42        default = "ResponseRequestDTO::default_tool_choice",
43        alias = "tool_choice"
44    )]
45    pub tool_choice: Value,
46    /// Optional end-user identifier forwarded to OpenAI for abuse-tracking.
47    pub user: Option<String>,
48    /// Sampling temperature. Forwarded verbatim; range not validated.
49    #[serde(default = "ResponseRequestDTO::default_temperature")]
50    pub temperature: f32,
51    /// Optional cap on completion tokens.
52    #[serde(alias = "max_completion_tokens")]
53    pub max_completion_tokens: Option<u32>,
54}
55
56impl ResponseRequestDTO {
57    fn default_tool_choice() -> Value {
58        Value::String("auto".into())
59    }
60    fn default_temperature() -> f32 {
61        1.0
62    }
63}
64
65/// Mirrors `cognee.api.v1.responses.models.ToolFunction`.
66#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
67pub struct ToolFunctionDTO {
68    /// Always `"function"` per OpenAI's schema.
69    #[serde(default = "ToolFunctionDTO::default_kind", rename = "type")]
70    pub kind: String,
71    pub function: FunctionDTO,
72}
73impl ToolFunctionDTO {
74    fn default_kind() -> String {
75        "function".into()
76    }
77}
78
79/// Mirrors `cognee.api.v1.responses.models.Function`.
80#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
81pub struct FunctionDTO {
82    pub name: String,
83    pub description: String,
84    pub parameters: FunctionParametersDTO,
85}
86
87/// Mirrors `cognee.api.v1.responses.models.FunctionParameters`.
88#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
89pub struct FunctionParametersDTO {
90    /// Always `"object"` per JSON Schema convention.
91    #[serde(default = "FunctionParametersDTO::default_type", rename = "type")]
92    pub kind: String,
93    pub properties: HashMap<String, Value>,
94    pub required: Option<Vec<String>>,
95}
96impl FunctionParametersDTO {
97    fn default_type() -> String {
98        "object".into()
99    }
100}
101
102// ─── Response ────────────────────────────────────────────────────────────────
103
104/// Mirrors `cognee.api.v1.responses.models.ResponseBody`.
105/// Stage B returns this; Stage A never constructs it (returns 501 instead).
106///
107/// Inherits `OutDTO` in Python — wire is camelCase (`toolCalls`).
108#[derive(Debug, Serialize, ToSchema)]
109#[serde(rename_all = "camelCase")]
110pub struct ResponseBodyDTO {
111    /// Server-generated id; format `resp_<hex>`.
112    pub id: String,
113    /// Unix epoch seconds at response assembly time.
114    pub created: i64,
115    /// Echoes the request's `model` field.
116    pub model: String,
117    /// Always `"response"`.
118    pub object: String,
119    /// Always `"completed"` in Stage B.
120    pub status: String,
121    /// One entry per dispatched `function_call` from the upstream output.
122    pub tool_calls: Vec<ResponseToolCallDTO>,
123    /// Token usage from the upstream call.
124    pub usage: Option<ChatUsageDTO>,
125    /// Reserved metadata. Always `null` today.
126    pub metadata: Option<HashMap<String, Value>>,
127}
128
129/// Mirrors `cognee.api.v1.responses.models.ResponseToolCall`.
130#[derive(Debug, Serialize, ToSchema)]
131pub struct ResponseToolCallDTO {
132    pub id: String,
133    /// Always `"function"`.
134    #[serde(rename = "type")]
135    pub kind: String,
136    pub function: FunctionCallDTO,
137    pub output: Option<ToolCallOutputDTO>,
138}
139
140/// Mirrors `cognee.api.v1.responses.models.FunctionCall`.
141#[derive(Debug, Serialize, ToSchema)]
142pub struct FunctionCallDTO {
143    pub name: String,
144    /// JSON-encoded string — a *string of JSON*, not a JSON object.
145    pub arguments: String,
146}
147
148/// Mirrors `cognee.api.v1.responses.models.ToolCallOutput`.
149#[derive(Debug, Serialize, ToSchema)]
150pub struct ToolCallOutputDTO {
151    /// `"success"` or `"error"`.
152    pub status: String,
153    pub data: Option<HashMap<String, Value>>,
154}
155
156/// Mirrors `cognee.api.v1.responses.models.ChatUsage`.
157/// Note: Python renames `input_tokens`/`output_tokens` from OpenAI's wire
158/// to `prompt_tokens`/`completion_tokens`.  We keep the rename for compat.
159#[derive(Debug, Serialize, Deserialize, ToSchema, Default)]
160pub struct ChatUsageDTO {
161    pub prompt_tokens: u32,
162    pub completion_tokens: u32,
163    pub total_tokens: u32,
164}
165
166// ─── Tests ───────────────────────────────────────────────────────────────────
167
168#[cfg(test)]
169#[allow(
170    clippy::unwrap_used,
171    clippy::expect_used,
172    reason = "test code — panics are acceptable failures"
173)]
174mod tests {
175    use super::*;
176    use serde_json::json;
177
178    #[test]
179    fn round_trip_response_request_dto_snake_case_via_alias() {
180        let input = json!({
181            "model": "cognee-v1",
182            "input": "What is the meaning of life?",
183            "tools": null,
184            "tool_choice": "auto",
185            "temperature": 1.0
186        });
187
188        let dto: ResponseRequestDTO =
189            serde_json::from_value(input).expect("deserialize ResponseRequestDTO");
190        assert_eq!(dto.input, "What is the meaning of life?");
191        assert_eq!(dto.model, CogneeModelDTO::CogneeV1);
192        assert_eq!(dto.temperature, 1.0);
193    }
194
195    #[test]
196    fn round_trip_response_request_dto_camelcase() {
197        let input = json!({
198            "model": "cognee-v1",
199            "input": "x",
200            "toolChoice": "auto",
201            "maxCompletionTokens": 64
202        });
203        let dto: ResponseRequestDTO = serde_json::from_value(input).expect("deserialize camelCase");
204        assert_eq!(dto.input, "x");
205        assert_eq!(dto.max_completion_tokens, Some(64));
206    }
207
208    #[test]
209    fn tool_choice_accepts_object_variant() {
210        let input = json!({
211            "input": "hello",
212            "tool_choice": {"type": "function", "function": {"name": "search"}}
213        });
214        let dto: ResponseRequestDTO =
215            serde_json::from_value(input).expect("deserialize with object tool_choice");
216        assert!(dto.tool_choice.is_object());
217    }
218
219    #[test]
220    fn response_body_dto_serializes_camelcase_only() {
221        let dto = ResponseBodyDTO {
222            id: "resp_1".into(),
223            created: 0,
224            model: "cognee-v1".into(),
225            object: "response".into(),
226            status: "completed".into(),
227            tool_calls: vec![],
228            usage: None,
229            metadata: None,
230        };
231        let s = serde_json::to_string(&dto).expect("serialize");
232        assert!(s.contains("\"toolCalls\""), "missing toolCalls: {s}");
233        assert!(
234            !s.contains("\"tool_calls\""),
235            "snake_case tool_calls leaked: {s}"
236        );
237    }
238}