opendev_models/
message.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use strum::{Display, EnumString};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
10#[serde(rename_all = "lowercase")]
11#[strum(serialize_all = "lowercase")]
12pub enum Role {
13 User,
14 Assistant,
15 System,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
20#[serde(rename_all = "snake_case")]
21#[strum(serialize_all = "snake_case")]
22pub enum ProvenanceKind {
23 ExternalUser,
25 InterSession,
27 InternalSystem,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct InputProvenance {
34 pub kind: ProvenanceKind,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub source_channel: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub source_session_id: Option<String>,
41 #[serde(default = "Utc::now", with = "crate::datetime_compat")]
42 pub timestamp: DateTime<Utc>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ToolCall {
48 pub id: String,
49 pub name: String,
50 pub parameters: HashMap<String, serde_json::Value>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub result: Option<serde_json::Value>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub result_summary: Option<String>,
56 #[serde(default = "Utc::now", with = "crate::datetime_compat")]
57 pub timestamp: DateTime<Utc>,
58 #[serde(default)]
59 pub approved: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub error: Option<String>,
62 #[serde(default)]
64 pub nested_tool_calls: Vec<ToolCall>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ChatMessage {
70 pub role: Role,
71 pub content: String,
72 #[serde(default = "Utc::now", with = "crate::datetime_compat")]
73 pub timestamp: DateTime<Utc>,
74 #[serde(default)]
75 pub metadata: HashMap<String, serde_json::Value>,
76 #[serde(default)]
77 pub tool_calls: Vec<ToolCall>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub tokens: Option<u64>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
84 pub thinking_trace: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub reasoning_content: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub token_usage: Option<HashMap<String, serde_json::Value>>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub provenance: Option<InputProvenance>,
95}
96
97impl ChatMessage {
98 pub fn token_estimate(&self) -> u64 {
100 if let Some(tokens) = self.tokens {
101 return tokens;
102 }
103 Self::compute_token_estimate(&self.content, &self.tool_calls)
104 }
105
106 pub fn cache_token_estimate(&mut self) -> u64 {
112 if let Some(tokens) = self.tokens {
113 return tokens;
114 }
115 let estimate = Self::compute_token_estimate(&self.content, &self.tool_calls);
116 self.tokens = Some(estimate);
117 estimate
118 }
119
120 fn compute_token_estimate(content: &str, tool_calls: &[ToolCall]) -> u64 {
122 let content_tokens = content.len() as u64 / 4;
124 let tool_tokens: u64 = tool_calls
125 .iter()
126 .map(|tc| {
127 let params_str = serde_json::to_string(&tc.parameters).unwrap_or_default();
128 params_str.len() as u64 / 4
129 })
130 .sum();
131 content_tokens + tool_tokens
132 }
133}
134
135#[cfg(test)]
136#[path = "message_tests.rs"]
137mod tests;