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}