Skip to main content

behest_core/
message.rs

1//! Provider-neutral chat request and response data structures.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::id::{ModelName, ProviderId};
7use crate::tool_types::{ToolCall, ToolChoice, ToolSpec};
8
9/// Request for a complete or streamed chat response.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ChatRequest {
12    /// Provider-specific model name.
13    pub model: ModelName,
14    /// Ordered conversation messages.
15    pub messages: Vec<Message>,
16    /// Tool definitions available to the model.
17    pub tools: Vec<ToolSpec>,
18    /// Tool selection policy.
19    pub tool_choice: ToolChoice,
20    /// Optional output format constraint.
21    pub response_format: Option<ResponseFormat>,
22    /// Sampling temperature.
23    pub temperature: Option<f32>,
24    /// Nucleus sampling probability.
25    pub top_p: Option<f32>,
26    /// Maximum output tokens.
27    pub max_output_tokens: Option<u32>,
28    /// Stop sequences.
29    pub stop: Vec<String>,
30    /// Application metadata forwarded to provider adapters.
31    pub metadata: Value,
32}
33
34impl ChatRequest {
35    /// Creates a chat request for the given model with no messages.
36    #[must_use]
37    pub fn new(model: ModelName) -> Self {
38        Self {
39            model,
40            messages: Vec::new(),
41            tools: Vec::new(),
42            tool_choice: ToolChoice::default(),
43            response_format: None,
44            temperature: None,
45            top_p: None,
46            max_output_tokens: None,
47            stop: Vec::new(),
48            metadata: Value::Null,
49        }
50    }
51
52    /// Appends one message to the request.
53    #[must_use]
54    pub fn with_message(mut self, message: Message) -> Self {
55        self.messages.push(message);
56        self
57    }
58
59    /// Appends a user text message to the request.
60    #[must_use]
61    pub fn with_user_text(self, text: impl Into<String>) -> Self {
62        self.with_message(Message::user_text(text))
63    }
64
65    /// Adds a tool definition to the request.
66    #[must_use]
67    pub fn with_tool(mut self, tool: ToolSpec) -> Self {
68        self.tools.push(tool);
69        self
70    }
71}
72
73/// Chat message role and content.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case", tag = "role")]
76#[non_exhaustive]
77pub enum Message {
78    /// System instruction message.
79    System {
80        /// Message content parts.
81        content: Vec<ContentPart>,
82    },
83    /// User message.
84    User {
85        /// Message content parts.
86        content: Vec<ContentPart>,
87    },
88    /// Assistant message, optionally including tool calls.
89    Assistant {
90        /// Message content parts.
91        content: Vec<ContentPart>,
92        /// Tool calls requested by the assistant.
93        tool_calls: Vec<ToolCall>,
94    },
95    /// Tool result message.
96    Tool {
97        /// Tool call identifier being answered.
98        tool_call_id: String,
99        /// Tool name that produced the result.
100        name: String,
101        /// Tool result content parts.
102        content: Vec<ContentPart>,
103    },
104}
105
106impl Message {
107    /// Creates a system text message.
108    #[must_use]
109    pub fn system_text(text: impl Into<String>) -> Self {
110        Self::System {
111            content: vec![ContentPart::text(text)],
112        }
113    }
114
115    /// Creates a user text message.
116    #[must_use]
117    pub fn user_text(text: impl Into<String>) -> Self {
118        Self::User {
119            content: vec![ContentPart::text(text)],
120        }
121    }
122
123    /// Creates an assistant text message without tool calls.
124    #[must_use]
125    pub fn assistant_text(text: impl Into<String>) -> Self {
126        Self::Assistant {
127            content: vec![ContentPart::text(text)],
128            tool_calls: Vec::new(),
129        }
130    }
131
132    /// Creates a tool result message.
133    #[must_use]
134    pub fn tool_text(
135        tool_call_id: impl Into<String>,
136        name: impl Into<String>,
137        text: impl Into<String>,
138    ) -> Self {
139        Self::Tool {
140            tool_call_id: tool_call_id.into(),
141            name: name.into(),
142            content: vec![ContentPart::text(text)],
143        }
144    }
145
146    /// Returns the tool calls from an Assistant message, or empty slice.
147    #[must_use]
148    pub fn tool_calls(&self) -> &[ToolCall] {
149        match self {
150            Self::Assistant { tool_calls, .. } => tool_calls.as_slice(),
151            _ => &[],
152        }
153    }
154}
155
156/// A single content part inside a chat message.
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case", tag = "type")]
159#[non_exhaustive]
160pub enum ContentPart {
161    /// Plain text content.
162    Text {
163        /// Text payload.
164        text: String,
165    },
166    /// JSON value content.
167    Json {
168        /// JSON payload.
169        value: Value,
170    },
171    /// Image referenced by URL.
172    ImageUrl {
173        /// Public or provider-accessible image URL.
174        url: String,
175        /// Optional MIME type hint.
176        mime_type: Option<String>,
177    },
178}
179
180impl ContentPart {
181    /// Creates a text content part.
182    #[must_use]
183    pub fn text(text: impl Into<String>) -> Self {
184        Self::Text { text: text.into() }
185    }
186
187    /// Creates a JSON content part.
188    #[must_use]
189    pub fn json(value: Value) -> Self {
190        Self::Json { value }
191    }
192
193    /// Creates an image URL content part.
194    #[must_use]
195    pub fn image_url(url: impl Into<String>, mime_type: Option<String>) -> Self {
196        Self::ImageUrl {
197            url: url.into(),
198            mime_type,
199        }
200    }
201}
202
203/// Output format requested from a chat provider.
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case", tag = "type")]
206#[non_exhaustive]
207pub enum ResponseFormat {
208    /// Unconstrained text output.
209    Text,
210    /// Provider-native JSON object mode.
211    JsonObject,
212    /// Provider-native JSON schema mode.
213    JsonSchema {
214        /// Schema name sent to the provider.
215        name: String,
216        /// JSON schema document.
217        schema: Value,
218        /// Whether provider should strictly enforce the schema.
219        strict: bool,
220    },
221}
222
223/// Complete chat response from a provider.
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub struct ChatResponse {
226    /// Provider that produced the response.
227    pub provider: ProviderId,
228    /// Model that produced the response.
229    pub model: ModelName,
230    /// Assistant message returned by the provider.
231    pub message: Message,
232    /// Reason the provider stopped generating.
233    pub finish_reason: FinishReason,
234    /// Token accounting, when supplied by the provider.
235    pub usage: Option<TokenUsage>,
236    /// Raw provider response for adapters that retain it.
237    pub raw: Option<Value>,
238}
239
240/// Reason generation ended.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243#[non_exhaustive]
244pub enum FinishReason {
245    /// Natural stop condition.
246    Stop,
247    /// Provider stopped because tool calls were produced.
248    ToolCalls,
249    /// Maximum token limit was reached.
250    Length,
251    /// Provider content filter interrupted generation.
252    ContentFilter,
253    /// Provider reported an error after partial generation.
254    Error,
255    /// Provider-specific finish reason.
256    Unknown(String),
257}
258
259/// Token usage reported by a provider.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub struct TokenUsage {
262    /// Number of input tokens.
263    pub input_tokens: u64,
264    /// Number of output tokens.
265    pub output_tokens: u64,
266    /// Total token count.
267    pub total_tokens: u64,
268}
269
270impl TokenUsage {
271    /// Creates token usage and computes the total.
272    #[must_use]
273    pub const fn new(input_tokens: u64, output_tokens: u64) -> Self {
274        Self {
275            input_tokens,
276            output_tokens,
277            total_tokens: input_tokens + output_tokens,
278        }
279    }
280}