Skip to main content

scv_core/
message.rs

1//! What the model sees: the canonical history messages and the user's input.
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(tag = "role", rename_all = "snake_case")]
10pub enum Message {
11    User {
12        content: String,
13        /// Images that come with the text, for a model that accepts image
14        /// input. History keeps their paths, not their bytes.
15        #[serde(default, skip_serializing_if = "Vec::is_empty")]
16        images: Vec<ImageInput>,
17    },
18    Assistant {
19        content: String,
20        #[serde(default, skip_serializing_if = "Vec::is_empty")]
21        tool_calls: Vec<ToolCall>,
22    },
23    Tool {
24        call_id: String,
25        name: String,
26        content: String,
27        is_error: bool,
28    },
29    HistoryNote {
30        content: String,
31    },
32}
33
34impl Message {
35    /// A user message of plain text.
36    pub fn user(content: impl Into<String>) -> Self {
37        Self::User {
38            content: content.into(),
39            images: Vec::new(),
40        }
41    }
42
43    /// Estimated context cost: the serialized size in tokens, plus a small
44    /// per-message overhead and a fixed amount per image.
45    pub(crate) fn estimated_tokens(&self, bytes_per_token: usize) -> usize {
46        let bytes = serde_json::to_vec(self).map_or(0, |value| value.len());
47        let images = match self {
48            Self::User { images, .. } => images.len(),
49            _ => 0,
50        };
51        bytes
52            .div_ceil(bytes_per_token)
53            .saturating_add(4)
54            .saturating_add(images.saturating_mul(IMAGE_TOKENS))
55    }
56}
57
58/// What one image costs in context, whatever its size: providers scale
59/// images down to a bounded number of tiles.
60pub(crate) const IMAGE_TOKENS: usize = 1600;
61
62/// An image file shown to the model with a user message.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ImageInput {
65    /// Absolute path on the host; the provider reads it for each request, so
66    /// an image removed since is replaced by a note.
67    pub path: PathBuf,
68    /// MIME type, such as `image/png`.
69    pub mime: String,
70}
71
72/// The user's side of a turn: text, and images for a model that accepts them.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct TurnInput {
75    pub text: String,
76    pub images: Vec<ImageInput>,
77}
78
79impl From<String> for TurnInput {
80    fn from(text: String) -> Self {
81        Self {
82            text,
83            images: Vec::new(),
84        }
85    }
86}
87
88impl From<&str> for TurnInput {
89    fn from(text: &str) -> Self {
90        text.to_owned().into()
91    }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95pub struct ToolCall {
96    pub id: String,
97    pub name: String,
98    pub arguments: Value,
99}
100
101#[cfg(test)]
102mod tests;