deepseek_rust_cli/
config.rs1use std::{fs, path::PathBuf};
2
3use anyhow::Result;
4use dotenvy::dotenv;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct Config {
9 pub model: String,
10 pub base_url: String,
11 pub request_timeout: u64,
12 #[serde(default)]
13 pub proxy_url: Option<String>,
14 #[serde(default)]
15 pub proxy_username: Option<String>,
16 #[serde(default)]
17 pub proxy_password: Option<String>,
18 #[serde(default)]
19 pub danger_accept_invalid_certs: bool,
20 pub temperature: f32,
21 pub top_p: f32,
22 pub presence_penalty: f32,
23 pub frequency_penalty: f32,
24 pub max_tokens: u32,
25 pub max_iterations: usize,
26 pub show_token_usage: bool,
27 pub concise_reasoning: bool,
28 pub debug: bool,
29 pub system_prompt: String,
30 #[serde(default = "default_max_tool_output_chars")]
31 pub max_tool_output_chars: usize,
32 #[serde(default = "default_max_context_chars")]
33 pub max_context_chars: usize,
34 #[serde(default = "default_thinking_enabled")]
35 pub thinking_enabled: bool,
36 #[serde(default = "default_reasoning_effort")]
37 pub reasoning_effort: Option<String>,
38 #[serde(default)]
39 pub json_mode: bool,
40}
41
42impl Default for Config {
43 fn default() -> Self {
44 let os = std::env::consts::OS;
45 let shell = std::env::var("SHELL").unwrap_or_else(|_| {
46 if os == "windows" {
47 "cmd/powershell".to_string()
48 } else {
49 "sh".to_string()
50 }
51 });
52
53 let prompt = DEFAULT_SYSTEM_PROMPT
54 .replace("{os}", os)
55 .replace("{shell}", &shell);
56
57 Self {
58 model: "deepseek-v4-pro".to_string(), base_url: "https://api.deepseek.com".to_string(),
61 request_timeout: 6000, proxy_url: None,
63 proxy_username: None,
64 proxy_password: None,
65 danger_accept_invalid_certs: false,
66 temperature: 0.0,
67 top_p: 1.0,
68 presence_penalty: 0.0,
69 frequency_penalty: 0.0,
70 max_tokens: 16_384, max_iterations: 500,
72 show_token_usage: true,
73 concise_reasoning: true,
74 debug: false,
75 system_prompt: prompt,
76 max_tool_output_chars: 15000,
77 max_context_chars: 100000,
78 thinking_enabled: true,
79 reasoning_effort: Some("high".to_string()),
80 json_mode: false,
81 }
82 }
83}
84
85fn default_max_tool_output_chars() -> usize {
86 15000
87}
88
89fn default_max_context_chars() -> usize {
90 100000
91}
92
93fn default_thinking_enabled() -> bool {
94 true
95}
96
97fn default_reasoning_effort() -> Option<String> {
98 Some("high".to_string())
99}
100const DEFAULT_SYSTEM_PROMPT: &str =
101 "You are a terminal-based AI coding assistant running on {os} via {shell}.
102Be concise and practical. You have full access to the workspace to read/write files and execute \
103 commands.
104Explain your actions briefly. To save tokens: do not print full file contents in your response \
105 (use diffs/summaries instead), do not repeat tool outputs verbatim, and keep reasoning and \
106 responses as short as possible.";
107
108pub fn init_workspace() {
112 let deep_dir = PathBuf::from(".deep");
113
114 if !deep_dir.exists() {
116 let _ = fs::create_dir_all(&deep_dir);
117 }
118
119 let history_dir = deep_dir.join("history");
121 if !history_dir.exists() {
122 let _ = fs::create_dir_all(&history_dir);
123 }
124
125 let config_path = deep_dir.join("config.json");
127 if !config_path.exists() {
128 let config = Config::default();
129 if let Ok(json) = serde_json::to_string_pretty(&config) {
130 let _ = fs::write(&config_path, json);
131 }
132 }
133
134 let memory_path = deep_dir.join("memory.md");
136 if !memory_path.exists() {
137 let default_memory = r#"# Local Memory
138
139This file serves as the agent's persistent memory for this project.
140You can update this file to store important context, decisions, and notes
141that the AI agent should remember across sessions.
142
143## Project Notes
144-
145
146## Decisions
147-
148
149## Important Context
150-
151"#;
152 let _ = fs::write(&memory_path, default_memory);
153 }
154}
155
156pub fn load_config() -> Config {
157 if let Ok(content) = fs::read_to_string(".deep/config.json") {
159 if let Ok(loaded) = serde_json::from_str::<Config>(&content) {
160 return loaded;
161 }
162 }
163
164 if let Some(mut home) = dirs::home_dir() {
166 home.push(".deep/config.json");
167 if let Some(loaded) = fs::read_to_string(home)
168 .ok()
169 .and_then(|c| serde_json::from_str::<Config>(&c).ok())
170 {
171 return loaded;
172 }
173 }
174
175 Config::default()
176}
177
178impl Config {
179 pub fn save(&self) -> Result<()> {
180 let path = PathBuf::from(".deep/config.json");
181 let json = serde_json::to_string_pretty(self)?;
182 fs::write(path, json)?;
183 Ok(())
184 }
185}
186
187pub fn get_api_key() -> Result<String> {
188 dotenv().ok();
189
190 if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
192 return Ok(key);
193 }
194
195 if let Some(mut home) = dirs::home_dir() {
197 home.push(".deep/.env");
198 if home.exists() {
199 let _ = dotenvy::from_path(&home);
200 if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
201 return Ok(key);
202 }
203 }
204 }
205
206 anyhow::bail!(
207 "DEEPSEEK_API_KEY not found.\nPlease create ~/.deep/.env or workspace .env \
208 with:\nDEEPSEEK_API_KEY=your_api_key_here"
209 )
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_default_config() {
218 let config = Config::default();
219 assert_eq!(config.model, "deepseek-v4-pro");
220 assert!(config.temperature >= 0.0);
221 }
222
223 #[test]
224 fn test_config_serialization() {
225 let config = Config::default();
226 let json = serde_json::to_string(&config).unwrap();
227 let decoded: Config = serde_json::from_str(&json).unwrap();
228 assert_eq!(config.model, decoded.model);
229 assert_eq!(config.max_tool_output_chars, decoded.max_tool_output_chars);
230 assert_eq!(config.max_context_chars, decoded.max_context_chars);
231 }
232
233 #[test]
234 fn test_config_backward_compatibility() {
235 let json = r#"{"model":"test-model","base_url":"http://test","request_timeout":10,"temperature":0.5,"top_p":0.9,"presence_penalty":0.0,"frequency_penalty":0.0,"max_tokens":1000,"max_iterations":5,"show_token_usage":false,"concise_reasoning":false,"debug":false,"system_prompt":"sys"}"#;
236 let decoded: Config = serde_json::from_str(json).unwrap();
237 assert_eq!(decoded.max_tool_output_chars, 15000);
238 assert_eq!(decoded.max_context_chars, 100000);
239 }
240}