zeroclaw_config/schema/
v1.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use crate::migration::fold_string_into_array;
5use crate::schema::v2::V2Config;
6
7#[derive(Debug, Default, Deserialize, Serialize)]
10pub struct V1Config {
11 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pub api_key: Option<toml::Value>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub api_url: Option<toml::Value>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub api_path: Option<toml::Value>,
17 #[serde(
18 default,
19 skip_serializing_if = "Option::is_none",
20 alias = "model_provider"
21 )]
22 pub default_provider: Option<toml::Value>,
23 #[serde(default, skip_serializing_if = "Option::is_none", alias = "model")]
24 pub default_model: Option<toml::Value>,
25 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
26 pub model_providers: HashMap<String, toml::Value>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub default_temperature: Option<toml::Value>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub provider_timeout_secs: Option<toml::Value>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub provider_max_tokens: Option<toml::Value>,
33 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
34 pub extra_headers: HashMap<String, toml::Value>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub model_routes: Vec<toml::Value>,
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub embedding_routes: Vec<toml::Value>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub channels_config: Option<toml::Value>,
42
43 #[serde(flatten)]
44 pub passthrough: toml::Table,
45}
46
47impl V1Config {
48 pub fn migrate(self) -> V2Config {
49 let V1Config {
50 api_key,
51 api_url,
52 api_path,
53 default_provider,
54 default_model,
55 model_providers,
56 default_temperature,
57 provider_timeout_secs,
58 provider_max_tokens,
59 extra_headers,
60 model_routes,
61 embedding_routes,
62 channels_config,
63 mut passthrough,
64 } = self;
65
66 let has_v1_providers_data = default_provider.is_some()
70 || default_model.is_some()
71 || api_key.is_some()
72 || api_url.is_some()
73 || api_path.is_some()
74 || default_temperature.is_some()
75 || provider_timeout_secs.is_some()
76 || provider_max_tokens.is_some()
77 || !extra_headers.is_empty()
78 || !model_providers.is_empty()
79 || !model_routes.is_empty()
80 || !embedding_routes.is_empty();
81
82 let providers_value = if !has_v1_providers_data {
83 None
84 } else {
85 let default_provider_key: String = default_provider
89 .as_ref()
90 .and_then(|v| v.as_str())
91 .map(str::to_string)
92 .unwrap_or_else(|| "openrouter".to_string());
93
94 let mut models_table: toml::Table = model_providers.into_iter().collect();
95
96 let needs_fold = api_key.is_some()
97 || api_url.is_some()
98 || api_path.is_some()
99 || default_model.is_some()
100 || default_temperature.is_some()
101 || provider_timeout_secs.is_some()
102 || provider_max_tokens.is_some()
103 || !extra_headers.is_empty();
104
105 if needs_fold {
106 let entry_value = models_table
107 .remove(&default_provider_key)
108 .unwrap_or_else(|| toml::Value::Table(toml::Table::new()));
109 let mut entry_table = match entry_value {
110 toml::Value::Table(t) => t,
111 other => {
112 models_table.insert(default_provider_key.clone(), other);
114 toml::Table::new()
115 }
116 };
117
118 if let Some(v) = api_key {
122 entry_table.entry("api_key".to_string()).or_insert(v);
123 }
124 if let Some(v) = api_url {
125 entry_table.entry("base_url".to_string()).or_insert(v);
126 }
127 if let Some(v) = api_path {
128 entry_table.entry("api_path".to_string()).or_insert(v);
129 }
130 if let Some(v) = default_model {
131 entry_table.entry("model".to_string()).or_insert(v);
132 }
133 if let Some(v) = default_temperature {
134 entry_table.entry("temperature".to_string()).or_insert(v);
135 }
136 if let Some(v) = provider_timeout_secs {
137 entry_table.entry("timeout_secs".to_string()).or_insert(v);
138 }
139 if let Some(v) = provider_max_tokens {
140 entry_table.entry("max_tokens".to_string()).or_insert(v);
141 }
142 if !extra_headers.is_empty() {
143 let headers_table: toml::Table = extra_headers.into_iter().collect();
144 entry_table
145 .entry("extra_headers".to_string())
146 .or_insert_with(|| toml::Value::Table(headers_table));
147 }
148
149 models_table.insert(
150 default_provider_key.clone(),
151 toml::Value::Table(entry_table),
152 );
153
154 ::zeroclaw_log::record!(
155 INFO,
156 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
157 .with_attrs(
158 ::serde_json::json!({"default_provider_key": default_provider_key})
159 ),
160 "V1 top-level provider globals folded into [providers.models.]"
161 );
162 }
163
164 let mut providers = toml::Table::new();
165 providers.insert(
166 "fallback".to_string(),
167 toml::Value::String(default_provider_key),
168 );
169 if !models_table.is_empty() {
170 providers.insert("models".to_string(), toml::Value::Table(models_table));
171 }
172 if !model_routes.is_empty() {
173 providers.insert("model_routes".to_string(), toml::Value::Array(model_routes));
174 }
175 if !embedding_routes.is_empty() {
176 providers.insert(
177 "embedding_routes".to_string(),
178 toml::Value::Array(embedding_routes),
179 );
180 }
181 Some(toml::Value::Table(providers))
182 };
183
184 if let Some(mut channels_value) = channels_config {
187 if let Some(channels_table) = channels_value.as_table_mut() {
188 apply_v1_to_v2_channel_folds(channels_table);
189 }
190 passthrough.insert("channels".to_string(), channels_value);
191 ::zeroclaw_log::record!(
192 INFO,
193 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
194 "channels_config → channels"
195 );
196 }
197
198 let mut v2 = V2Config {
199 schema_version: 2,
200 providers: providers_value,
201 passthrough,
202 ..V2Config::default()
203 };
204
205 if let Some(v) = v2.passthrough.remove("autonomy") {
208 v2.autonomy = Some(v);
209 }
210 if let Some(v) = v2.passthrough.remove("agent") {
211 v2.agent = Some(v);
212 }
213 if let Some(toml::Value::Table(t)) = v2.passthrough.remove("swarms") {
214 v2.swarms = t.into_iter().collect();
215 }
216 if let Some(v) = v2.passthrough.remove("cron") {
217 v2.cron = Some(v);
218 }
219 if let Some(v) = v2.passthrough.remove("cost") {
220 v2.cost = Some(v);
221 }
222 if let Some(v) = v2.passthrough.remove("channels") {
223 v2.channels = Some(v);
224 }
225 if let Some(toml::Value::Table(t)) = v2.passthrough.remove("agents") {
226 v2.agents = t.into_iter().collect();
227 }
228 if let Some(toml::Value::Table(user_providers)) = v2.passthrough.remove("providers") {
232 let synthesized = v2
233 .providers
234 .take()
235 .and_then(|v| match v {
236 toml::Value::Table(t) => Some(t),
237 _ => None,
238 })
239 .unwrap_or_default();
240 let mut merged = user_providers;
241 for (k, v) in synthesized {
242 merged.insert(k, v);
243 }
244 if !merged.is_empty() {
245 v2.providers = Some(toml::Value::Table(merged));
246 }
247 }
248
249 v2
250 }
251}
252
253fn apply_v1_to_v2_channel_folds(channels: &mut toml::Table) {
257 if let Some(toml::Value::Table(matrix)) = channels.get_mut("matrix")
258 && fold_string_into_array(matrix, "room_id", "allowed_rooms")
259 {
260 ::zeroclaw_log::record!(
261 INFO,
262 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
263 "channels.matrix.room_id folded into channels.matrix.allowed_rooms[]"
264 );
265 }
266 if let Some(toml::Value::Table(slack)) = channels.get_mut("slack")
267 && fold_string_into_array(slack, "channel_id", "channel_ids")
268 {
269 ::zeroclaw_log::record!(
270 INFO,
271 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
272 "channels.slack.channel_id folded into channels.slack.channel_ids[]"
273 );
274 }
275}