Skip to main content

vv_agent/
config.rs

1use std::fs;
2use std::path::Path;
3
4use serde_json::Value;
5use thiserror::Error;
6
7use crate::types::AgentTask;
8
9mod api_keys;
10mod model_resolution;
11mod settings_literal;
12
13pub use api_keys::decode_api_key;
14pub use model_resolution::{
15    build_vv_llm_from_local_settings, build_vv_llm_settings, resolve_model_endpoint,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct EndpointConfig {
20    pub endpoint_id: String,
21    pub api_key: String,
22    pub api_base: String,
23    pub endpoint_type: String,
24}
25
26impl EndpointConfig {
27    pub fn new(
28        endpoint_id: impl Into<String>,
29        api_key: impl Into<String>,
30        api_base: impl Into<String>,
31    ) -> Self {
32        Self {
33            endpoint_id: endpoint_id.into(),
34            api_key: api_key.into(),
35            api_base: api_base.into(),
36            endpoint_type: "default".to_string(),
37        }
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
42pub struct EndpointOption {
43    pub endpoint: EndpointConfig,
44    pub model_id: String,
45}
46
47impl EndpointOption {
48    pub fn new(endpoint: EndpointConfig, model_id: impl Into<String>) -> Self {
49        Self {
50            endpoint,
51            model_id: model_id.into(),
52        }
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
57pub struct ResolvedModelConfig {
58    pub backend: String,
59    pub requested_model: String,
60    pub selected_model: String,
61    pub model_id: String,
62    pub context_length: Option<u64>,
63    pub max_output_tokens: Option<u64>,
64    pub endpoint_options: Vec<EndpointOption>,
65}
66
67#[derive(Debug, Clone, Default, PartialEq, Eq)]
68pub struct MemorySummaryDefaults {
69    pub backend: Option<String>,
70    pub model: Option<String>,
71}
72
73impl ResolvedModelConfig {
74    pub fn new(
75        backend: impl Into<String>,
76        requested_model: impl Into<String>,
77        selected_model: impl Into<String>,
78        model_id: impl Into<String>,
79        endpoint_options: Vec<EndpointOption>,
80    ) -> Self {
81        Self {
82            backend: backend.into(),
83            requested_model: requested_model.into(),
84            selected_model: selected_model.into(),
85            model_id: model_id.into(),
86            context_length: None,
87            max_output_tokens: None,
88            endpoint_options,
89        }
90    }
91
92    pub fn with_token_limits(
93        mut self,
94        context_length: Option<u64>,
95        max_output_tokens: Option<u64>,
96    ) -> Self {
97        self.context_length = context_length;
98        self.max_output_tokens = max_output_tokens;
99        self
100    }
101
102    pub fn endpoint(&self) -> Option<&EndpointConfig> {
103        self.endpoint_options.first().map(|option| &option.endpoint)
104    }
105}
106
107pub fn apply_resolved_model_limits(task: &mut AgentTask, resolved: &ResolvedModelConfig) {
108    if let Some(context_length) = resolved.context_length {
109        task.metadata
110            .entry("model_context_window".to_string())
111            .or_insert_with(|| Value::from(context_length));
112    }
113    if let Some(max_output_tokens) = resolved.max_output_tokens {
114        task.metadata
115            .entry("reserved_output_tokens".to_string())
116            .or_insert_with(|| Value::from(max_output_tokens));
117    }
118}
119
120pub fn load_memory_summary_defaults_from_file(path: &Path) -> MemorySummaryDefaults {
121    let Ok(source) = fs::read_to_string(path) else {
122        return MemorySummaryDefaults::default();
123    };
124    MemorySummaryDefaults {
125        backend: parse_string_setting(
126            &source,
127            &[
128                "DEFAULT_USER_MEMORY_SUMMARIZE_BACKEND",
129                "DEFAULT_MEMORY_SUMMARIZE_BACKEND",
130                "VV_AGENT_MEMORY_SUMMARY_BACKEND",
131                "memory_summary_backend",
132                "compress_memory_summary_backend",
133            ],
134        ),
135        model: parse_string_setting(
136            &source,
137            &[
138                "DEFAULT_USER_MEMORY_SUMMARIZE_MODEL",
139                "DEFAULT_MEMORY_SUMMARIZE_MODEL",
140                "VV_AGENT_MEMORY_SUMMARY_MODEL",
141                "memory_summary_model",
142                "compress_memory_summary_model",
143            ],
144        ),
145    }
146}
147
148fn parse_string_setting(source: &str, targets: &[&str]) -> Option<String> {
149    parse_json_string_setting(source, targets)
150        .or_else(|| settings_literal::parse_string_assignment(source, targets))
151}
152
153fn parse_json_string_setting(source: &str, targets: &[&str]) -> Option<String> {
154    let value = serde_json::from_str::<Value>(source).ok()?;
155    for target in targets {
156        let Some(raw) = value.get(*target).and_then(Value::as_str) else {
157            continue;
158        };
159        let trimmed = raw.trim();
160        if !trimmed.is_empty() {
161            return Some(trimmed.to_string());
162        }
163    }
164    None
165}
166
167#[derive(Debug, Error)]
168pub enum ConfigError {
169    #[error("settings file not found: {0}")]
170    MissingSettingsFile(String),
171    #[error("failed to read settings file {path}: {source}")]
172    Io {
173        path: String,
174        #[source]
175        source: std::io::Error,
176    },
177    #[error("failed to parse settings file {path}: {source}")]
178    Parse {
179        path: String,
180        #[source]
181        source: Box<dyn std::error::Error + Send + Sync>,
182    },
183    #[error("invalid LLM settings: {0}")]
184    InvalidSettings(String),
185    #[error("unsupported chat backend: {0}")]
186    UnsupportedBackend(String),
187}
188
189pub fn load_llm_settings_from_file(path: impl AsRef<Path>) -> Result<Value, ConfigError> {
190    let path = path.as_ref();
191    if !path.exists() {
192        return Err(ConfigError::MissingSettingsFile(path.display().to_string()));
193    }
194
195    let content = fs::read_to_string(path).map_err(|source| ConfigError::Io {
196        path: path.display().to_string(),
197        source,
198    })?;
199
200    match path.extension().and_then(|ext| ext.to_str()) {
201        Some("py") => settings_literal::parse_llm_settings_source(&content).map_err(|source| {
202            ConfigError::Parse {
203                path: path.display().to_string(),
204                source: Box::new(source),
205            }
206        }),
207        Some("json") => serde_json::from_str(&content).map_err(|source| ConfigError::Parse {
208            path: path.display().to_string(),
209            source: Box::new(source),
210        }),
211        Some("toml") => {
212            let value: toml::Value =
213                toml::from_str(&content).map_err(|source| ConfigError::Parse {
214                    path: path.display().to_string(),
215                    source: Box::new(source),
216                })?;
217            serde_json::to_value(value).map_err(|source| ConfigError::Parse {
218                path: path.display().to_string(),
219                source: Box::new(source),
220            })
221        }
222        _ => serde_json::from_str(&content).map_err(|source| ConfigError::Parse {
223            path: path.display().to_string(),
224            source: Box::new(source),
225        }),
226    }
227}