Skip to main content

llm_universal/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Author of a message.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum Role {
8    System,
9    User,
10    Assistant,
11    /// Tool results. Sent as role `tool` to OpenAI and as `tool_result` blocks in a
12    /// `user` turn to Anthropic.
13    Tool,
14}
15
16/// One piece of a message.
17///
18/// Thinking blocks are not represented: if you enable extended thinking on Anthropic,
19/// keep the raw response yourself so the signed blocks can be sent back unchanged.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum Part {
23    Text {
24        text: String,
25    },
26    ToolCall {
27        id: String,
28        name: String,
29        /// Parsed arguments. If a provider returns arguments that are not valid JSON
30        /// (for example cut off by `max_tokens`), the raw string is kept as `Value::String`.
31        arguments: Value,
32    },
33    ToolResult {
34        call_id: String,
35        content: String,
36        is_error: bool,
37    },
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct Message {
42    pub role: Role,
43    pub parts: Vec<Part>,
44}
45
46impl Message {
47    pub fn new(role: Role, parts: Vec<Part>) -> Self {
48        Self { role, parts }
49    }
50
51    pub fn user(text: impl Into<String>) -> Self {
52        Self::text(Role::User, text)
53    }
54
55    pub fn assistant(text: impl Into<String>) -> Self {
56        Self::text(Role::Assistant, text)
57    }
58
59    pub fn text(role: Role, text: impl Into<String>) -> Self {
60        Self::new(role, vec![Part::Text { text: text.into() }])
61    }
62
63    /// A single tool result. To return several results from one step, put them in one
64    /// message with `Message::new(Role::Tool, parts)`.
65    pub fn tool_result(
66        call_id: impl Into<String>,
67        content: impl Into<String>,
68        is_error: bool,
69    ) -> Self {
70        Self::new(
71            Role::Tool,
72            vec![Part::ToolResult {
73                call_id: call_id.into(),
74                content: content.into(),
75                is_error,
76            }],
77        )
78    }
79
80    /// All text parts joined together.
81    pub fn text_content(&self) -> String {
82        self.parts
83            .iter()
84            .filter_map(|p| match p {
85                Part::Text { text } => Some(text.as_str()),
86                _ => None,
87            })
88            .collect::<Vec<_>>()
89            .join("")
90    }
91}
92
93/// A function the model may call. `parameters` is a JSON Schema with `"type": "object"`.
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct Tool {
96    pub name: String,
97    pub description: String,
98    pub parameters: Value,
99}
100
101impl Tool {
102    pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
103        Self {
104            name: name.into(),
105            description: description.into(),
106            parameters,
107        }
108    }
109}
110
111/// Provider-neutral chat request.
112#[derive(Debug, Clone, Default)]
113pub struct ChatRequest {
114    pub model: String,
115    /// System prompt. Sent as a `system` message to OpenAI and as the top-level
116    /// `system` field to Anthropic.
117    pub system: Option<String>,
118    pub messages: Vec<Message>,
119    pub tools: Vec<Tool>,
120    /// Required by Anthropic; [`DEFAULT_MAX_TOKENS`] is used when unset.
121    pub max_tokens: Option<u32>,
122    /// Anthropic accepts 0.0–1.0, and newer Anthropic models accept only 1.0.
123    /// Leave unset unless the model supports it.
124    pub temperature: Option<f32>,
125}
126
127/// `max_tokens` sent to Anthropic when the request does not set one.
128pub const DEFAULT_MAX_TOKENS: u32 = 4096;
129
130impl ChatRequest {
131    pub fn new(model: impl Into<String>) -> Self {
132        Self {
133            model: model.into(),
134            ..Default::default()
135        }
136    }
137
138    pub fn system(mut self, system: impl Into<String>) -> Self {
139        self.system = Some(system.into());
140        self
141    }
142
143    pub fn message(mut self, message: Message) -> Self {
144        self.messages.push(message);
145        self
146    }
147
148    pub fn tool(mut self, tool: Tool) -> Self {
149        self.tools.push(tool);
150        self
151    }
152
153    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
154        self.max_tokens = Some(max_tokens);
155        self
156    }
157
158    pub fn temperature(mut self, temperature: f32) -> Self {
159        self.temperature = Some(temperature);
160        self
161    }
162}
163
164/// Why generation stopped, normalized across providers.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum StopReason {
167    /// Natural end of the turn (`stop` / `end_turn` / `stop_sequence`).
168    EndTurn,
169    /// The model wants tools to be called (`tool_calls` / `tool_use`).
170    ToolUse,
171    /// Token limit reached (`length` / `max_tokens` / `model_context_window_exceeded`).
172    MaxTokens,
173    /// A long-running turn was paused (`pause_turn`). Send the response back as-is to continue.
174    Paused,
175    /// The model declined for policy reasons (`refusal` / `content_filter`).
176    Refusal,
177    /// Any other value returned by the provider.
178    Other(String),
179}
180
181#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
182pub struct Usage {
183    pub input_tokens: u32,
184    pub output_tokens: u32,
185}
186
187#[derive(Debug, Clone, PartialEq)]
188pub struct ChatResponse {
189    pub model: String,
190    pub message: Message,
191    pub stop_reason: StopReason,
192    pub usage: Usage,
193}
194
195impl ChatResponse {
196    /// All text of the response.
197    pub fn text(&self) -> String {
198        self.message.text_content()
199    }
200
201    /// Tool calls as `(id, name, arguments)`.
202    pub fn tool_calls(&self) -> Vec<(String, String, Value)> {
203        self.message
204            .parts
205            .iter()
206            .filter_map(|p| match p {
207                Part::ToolCall {
208                    id,
209                    name,
210                    arguments,
211                } => Some((id.clone(), name.clone(), arguments.clone())),
212                _ => None,
213            })
214            .collect()
215    }
216}