Skip to main content

aacode_rs/llm/
types.rs

1// Copyright (c) 2026 xiefujin <490021684@qq.com>
2// Licensed under GPL-3.0, see LICENSE file for full license terms.
3
4//! Shared LLM types: chat messages, tool calls, response envelope.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// One conversation message. Mirrors the OpenAI chat message shape plus the
10/// optional `reasoning_content` used by DeepSeek/Kimi thinking models.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ChatMessage {
13    pub role: String, // system | user | assistant | tool
14    #[serde(default)]
15    pub content: String,
16    #[serde(skip_serializing_if = "Option::is_none", default)]
17    pub tool_calls: Option<Vec<ToolCall>>,
18    #[serde(skip_serializing_if = "Option::is_none", default)]
19    pub tool_call_id: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none", default)]
21    pub reasoning_content: Option<String>,
22}
23
24impl ChatMessage {
25    pub fn system(content: impl Into<String>) -> Self {
26        Self::simple("system", content)
27    }
28    pub fn user(content: impl Into<String>) -> Self {
29        Self::simple("user", content)
30    }
31    pub fn assistant(content: impl Into<String>) -> Self {
32        Self::simple("assistant", content)
33    }
34    pub fn simple(role: &str, content: impl Into<String>) -> Self {
35        ChatMessage {
36            role: role.to_string(),
37            content: content.into(),
38            tool_calls: None,
39            tool_call_id: None,
40            reasoning_content: None,
41        }
42    }
43    /// A tool result message answering a specific tool_call id.
44    pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
45        ChatMessage {
46            role: "tool".to_string(),
47            content: content.into(),
48            tool_calls: None,
49            tool_call_id: Some(tool_call_id.into()),
50            reasoning_content: None,
51        }
52    }
53    /// An assistant message that carries tool_calls.
54    pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
55        ChatMessage {
56            role: "assistant".to_string(),
57            content: content.into(),
58            tool_calls: Some(tool_calls),
59            tool_call_id: None,
60            reasoning_content: None,
61        }
62    }
63}
64
65/// A single tool/function call requested by the model.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct ToolCall {
68    pub id: String,
69    pub name: String,
70    /// Raw JSON arguments string (as returned by the API). Parse before use.
71    pub arguments: String,
72}
73
74impl ToolCall {
75    /// Parse the arguments string into a JSON value. Returns an empty object
76    /// on parse failure (mirrors the Python fallback of `{}`).
77    pub fn parsed_args(&self) -> Value {
78        serde_json::from_str(&self.arguments).unwrap_or_else(|_| serde_json::json!({}))
79    }
80}
81
82/// Normalized provider token usage for one model call.
83#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
84pub struct Usage {
85    pub input_tokens: u64,
86    pub output_tokens: u64,
87    pub total_tokens: u64,
88    pub cache_read_tokens: u64,
89    pub cache_write_tokens: u64,
90    pub cache_miss_tokens: u64,
91    pub reasoning_tokens: u64,
92}
93
94impl Usage {
95    /// Cache hit rate over the prompt (0.0 when the provider reports no cache
96    /// fields).
97    pub fn cache_hit_rate(&self) -> f64 {
98        let hit = self.cache_read_tokens as f64;
99        let miss = self.cache_miss_tokens as f64;
100        let denom = hit + miss;
101        if denom > 0.0 {
102            hit / denom
103        } else {
104            0.0
105        }
106    }
107}
108
109/// The result of one streamed model call.
110#[derive(Debug, Clone, Default)]
111pub struct LlmResponse {
112    pub text: String,
113    pub tool_calls: Vec<ToolCall>,
114    pub reasoning_content: Option<String>,
115    /// "length"/"max_tokens" indicates truncation.
116    pub finish_reason: Option<String>,
117    /// Provider-reported usage, when the gateway exposes it.
118    pub usage: Option<Usage>,
119}
120
121impl LlmResponse {
122    pub fn is_truncated(&self) -> bool {
123        matches!(
124            self.finish_reason.as_deref(),
125            Some("length") | Some("max_tokens") | Some("connection_closed")
126        )
127    }
128}