Skip to main content

starweaver_context/config/
model.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use super::PerThousandRatio;
6
7/// Model capabilities that influence tool and media behavior.
8#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ModelCapability {
11    /// Model can process images.
12    Vision,
13    /// Model can process video content.
14    VideoUnderstanding,
15    /// Model can process documents.
16    DocumentUnderstanding,
17    /// Model can process audio content.
18    AudioUnderstanding,
19    /// Model supports images by URL.
20    ImageUrl,
21    /// Model supports videos by URL.
22    VideoUrl,
23    /// Provider requires reasoning content in assistant messages.
24    ReasoningRequired,
25    /// Provider rejects foreign-provider reasoning content.
26    ReasoningForeignUnsupported,
27}
28
29/// Runtime model configuration stored on [`crate::AgentContext`].
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31pub struct ModelConfig {
32    /// Context window in tokens.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub context_window: Option<u64>,
35    /// Parts-per-thousand threshold where proactive context reminders should begin.
36    #[serde(default = "default_proactive_context_management_threshold")]
37    pub proactive_context_management_threshold: Option<PerThousandRatio>,
38    /// Parts-per-thousand threshold where compacting becomes urgent.
39    #[serde(default = "default_compact_threshold")]
40    pub compact_threshold: PerThousandRatio,
41    /// Cold-start gap in seconds before older tool returns are aggressively trimmed.
42    #[serde(default = "default_cold_start_trim_seconds")]
43    pub cold_start_trim_seconds: u64,
44    /// Whether stream retry recovery should resume after provider stream errors.
45    #[serde(default)]
46    pub stream_resume_on_error: bool,
47    /// Maximum stream retry resume attempts.
48    #[serde(default = "default_stream_resume_max_attempts")]
49    pub stream_resume_max_attempts: usize,
50    /// Optional prompt used when resuming a failed stream.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub stream_resume_prompt: Option<String>,
53    /// Maximum number of images retained for model input.
54    #[serde(default = "default_model_max_images")]
55    pub max_images: usize,
56    /// Maximum number of videos retained for model input.
57    #[serde(default = "default_model_max_videos")]
58    pub max_videos: usize,
59    /// Whether GIF input is supported.
60    #[serde(default = "default_true")]
61    pub support_gif: bool,
62    /// Maximum image bytes after base64 encoding before compression applies.
63    ///
64    /// A value of `0` disables the byte limit without disabling the independent
65    /// image dimension limit.
66    #[serde(default = "default_model_max_image_bytes")]
67    pub max_image_bytes: usize,
68    /// Maximum width or height accepted for model image input.
69    ///
70    /// A value of `0` disables the dimension limit without disabling the
71    /// independent encoded-byte limit.
72    #[serde(default = "default_model_max_image_dimension")]
73    pub max_image_dimension: usize,
74    /// Whether large images should be split where supported.
75    #[serde(default = "default_true")]
76    pub split_large_images: bool,
77    /// Maximum split image height.
78    #[serde(default = "default_image_split_max_height")]
79    pub image_split_max_height: usize,
80    /// Pixel overlap between split image segments.
81    #[serde(default = "default_image_split_overlap")]
82    pub image_split_overlap: usize,
83    /// Explicit model capabilities.
84    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
85    pub capabilities: BTreeSet<ModelCapability>,
86}
87
88impl Default for ModelConfig {
89    fn default() -> Self {
90        Self {
91            context_window: None,
92            proactive_context_management_threshold: default_proactive_context_management_threshold(
93            ),
94            compact_threshold: default_compact_threshold(),
95            cold_start_trim_seconds: default_cold_start_trim_seconds(),
96            stream_resume_on_error: false,
97            stream_resume_max_attempts: default_stream_resume_max_attempts(),
98            stream_resume_prompt: None,
99            max_images: default_model_max_images(),
100            max_videos: default_model_max_videos(),
101            support_gif: true,
102            max_image_bytes: default_model_max_image_bytes(),
103            max_image_dimension: default_model_max_image_dimension(),
104            split_large_images: true,
105            image_split_max_height: default_image_split_max_height(),
106            image_split_overlap: default_image_split_overlap(),
107            capabilities: BTreeSet::new(),
108        }
109    }
110}
111
112impl ModelConfig {
113    /// Return whether the config only contains default values.
114    #[must_use]
115    pub fn is_default(&self) -> bool {
116        self == &Self::default()
117    }
118
119    /// Return whether a context-window value should be rendered into runtime context.
120    #[must_use]
121    pub const fn has_runtime_context_window(&self) -> bool {
122        self.context_window.is_some()
123    }
124
125    /// Return whether a capability is present.
126    #[must_use]
127    pub fn has_capability(&self, capability: &ModelCapability) -> bool {
128        self.capabilities.contains(capability)
129    }
130
131    /// Return whether the model supports vision.
132    #[must_use]
133    pub fn has_vision(&self) -> bool {
134        self.has_capability(&ModelCapability::Vision)
135    }
136
137    /// Return whether the model supports video understanding.
138    #[must_use]
139    pub fn has_video_understanding(&self) -> bool {
140        self.has_capability(&ModelCapability::VideoUnderstanding)
141    }
142
143    /// Return whether the model supports audio understanding.
144    #[must_use]
145    pub fn has_audio_understanding(&self) -> bool {
146        self.has_capability(&ModelCapability::AudioUnderstanding)
147    }
148
149    /// Return whether the model supports document understanding.
150    #[must_use]
151    pub fn has_document_understanding(&self) -> bool {
152        self.has_capability(&ModelCapability::DocumentUnderstanding)
153    }
154
155    /// Merge non-empty values from another config into this one.
156    pub fn merge_from(&mut self, other: Self) {
157        if other.context_window.is_some() {
158            self.context_window = other.context_window;
159        }
160        self.proactive_context_management_threshold = other.proactive_context_management_threshold;
161        self.compact_threshold = other.compact_threshold;
162        self.cold_start_trim_seconds = other.cold_start_trim_seconds;
163        self.stream_resume_on_error = other.stream_resume_on_error;
164        self.stream_resume_max_attempts = other.stream_resume_max_attempts;
165        if other.stream_resume_prompt.is_some() {
166            self.stream_resume_prompt = other.stream_resume_prompt;
167        }
168        self.max_images = other.max_images;
169        self.max_videos = other.max_videos;
170        self.support_gif = other.support_gif;
171        self.max_image_bytes = other.max_image_bytes;
172        self.max_image_dimension = other.max_image_dimension;
173        self.split_large_images = other.split_large_images;
174        self.image_split_max_height = other.image_split_max_height;
175        self.image_split_overlap = other.image_split_overlap;
176        if !other.capabilities.is_empty() {
177            self.capabilities = other.capabilities;
178        }
179    }
180}
181
182const fn default_true() -> bool {
183    true
184}
185
186#[allow(clippy::unnecessary_wraps)]
187const fn default_proactive_context_management_threshold() -> Option<PerThousandRatio> {
188    Some(PerThousandRatio::from_per_thousand(650))
189}
190
191const fn default_compact_threshold() -> PerThousandRatio {
192    PerThousandRatio::from_per_thousand(900)
193}
194
195const fn default_cold_start_trim_seconds() -> u64 {
196    3600
197}
198
199const fn default_stream_resume_max_attempts() -> usize {
200    3
201}
202
203const fn default_model_max_images() -> usize {
204    20
205}
206
207const fn default_model_max_videos() -> usize {
208    1
209}
210
211const fn default_model_max_image_bytes() -> usize {
212    5 * 1024 * 1024
213}
214
215const fn default_model_max_image_dimension() -> usize {
216    8000
217}
218
219const fn default_image_split_max_height() -> usize {
220    4096
221}
222
223const fn default_image_split_overlap() -> usize {
224    50
225}