1use crate::cli::CliArgs;
10use crate::store::settings::Settings;
11use anyhow::Result;
12use std::path::PathBuf;
13use tracing;
14
15pub async fn build_app(args: &CliArgs) -> Result<crate::App> {
18 let mut settings = Settings::load().unwrap_or_default();
20
21 settings.merge_cli(
23 args.model.clone(),
24 args.provider.clone(),
25 Some(args.enable_routing),
26 Some(args.prefer_cost_efficient),
27 if args.fallback_chain.is_empty() {
28 None
29 } else {
30 Some(args.fallback_chain.clone())
31 },
32 Some(args.disable_fallback),
33 );
34
35 if settings
36 .effective_model(None)
37 .unwrap_or_default()
38 .is_empty()
39 {
40 eprintln!("No model configured. Run `oxi setup` to configure.");
41 std::process::exit(1);
42 }
43
44 register_custom_providers(&settings);
46
47 register_router_provider(&settings);
49
50 if let Some(ref level_str) = args.thinking {
52 if let Some(level) = crate::store::settings::parse_thinking_level(level_str) {
53 settings.thinking_level = level;
54 } else {
55 anyhow::bail!(
56 "Invalid thinking level: {}. Valid options: off, minimal, low, medium, high, xhigh",
57 level_str
58 );
59 }
60 }
61
62 let oxi = crate::build_oxi_engine()?;
64 let mut app = crate::App::from_oxi(oxi, settings).await?;
65
66 let tools = app.agent_tools();
68 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
69 register_builtin_tools(&tools, &cwd, args, &app.settings().disabled_tools);
70
71 let wasm_ext = load_wasm_extensions(&app, &cwd, &tools);
73 app.set_wasm_ext(wasm_ext);
74
75 if let Some(ref prompt_path) = args.append_system_prompt {
77 let content = std::fs::read_to_string(prompt_path)
78 .map_err(|e| anyhow::anyhow!("Failed to read system prompt file: {}", e))?;
79 app.agent().set_system_prompt(content);
80 }
81
82 Ok(app)
83}
84
85pub async fn dispatch_run_mode(args: &CliArgs, app: crate::App) -> Result<i32> {
87 let prompt = args.prompt.join(" ");
88
89 if args.mode.as_deref() == Some("json") || args.print {
90 let mode = if args.mode.as_deref() == Some("json") {
91 crate::print_mode::PrintMode::Json
92 } else {
93 crate::print_mode::PrintMode::Text
94 };
95 let options = crate::print_mode::PrintModeOptions {
96 mode,
97 initial_message: if prompt.is_empty() {
98 None
99 } else {
100 Some(prompt)
101 },
102 messages: vec![],
103 no_stdin: args.print,
104 no_session: args.print || args.no_session,
105 quiet: args.print,
106 timeout: args.timeout,
107 };
108 return crate::print_mode::run_print_mode(&app, options).await;
109 }
110
111 if prompt.is_empty() || args.interactive {
112 if args.continue_session {
113 crate::tui::run_tui_interactive_with_continue(app, true).await?;
114 } else {
115 crate::tui::run_tui_interactive(app).await?;
116 }
117 return Ok(0);
118 }
119
120 crate::main_dispatch::run_single_prompt(app, &prompt).await?;
121 Ok(0)
122}
123
124pub async fn run_with_args(args: CliArgs) -> Result<i32> {
126 let app = build_app(&args).await?;
127 dispatch_run_mode(&args, app).await
128}
129
130pub fn init_logging() {
137 let log_dir = dirs::cache_dir()
138 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
139 .join("oxi");
140 let _ = std::fs::create_dir_all(&log_dir);
141 let log_path = log_dir.join("oxi.log");
142
143 let log_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_string());
144 let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
145 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(&log_filter));
146
147 let log_file = std::fs::File::create(&log_path).expect("Failed to create log file");
148 let writer = std::sync::Mutex::new(log_file);
149
150 tracing_subscriber::fmt()
151 .with_env_filter(env_filter)
152 .with_writer(writer)
153 .with_target(true)
154 .with_thread_ids(true)
155 .with_ansi(false)
156 .init();
157
158 tracing::info!("Logging initialized, log file: {:?}", log_path);
159}
160
161fn register_custom_providers(settings: &Settings) {
163 let auth_storage = crate::store::auth_storage::shared_auth_storage();
164 for cp in &settings.custom_providers {
165 let api_key = auth_storage.get_api_key(&cp.name);
166 let api = cp.api.to_lowercase();
167
168 match api.as_str() {
169 "openai-completions" | "openai" => {
170 let provider =
171 oxi_ai::OpenAiProvider::with_base_url_and_key(&cp.base_url, api_key.clone());
172 oxi_sdk::register_provider(&cp.name, provider);
173 tracing::info!(
174 "Registered custom provider '{}' (openai-completions) -> {}",
175 cp.name,
176 cp.base_url
177 );
178 }
179 "openai-responses" | "responses" => {
180 let provider = oxi_sdk::OpenAiResponsesProvider::with_base_url_and_key(
181 &cp.base_url,
182 api_key.clone(),
183 );
184 oxi_sdk::register_provider(&cp.name, provider);
185 tracing::info!(
186 "Registered custom provider '{}' (openai-responses) -> {}",
187 cp.name,
188 cp.base_url
189 );
190 }
191 _ => {
192 tracing::warn!(
193 "Unknown API type '{}' for custom provider '{}'. Supported: openai-completions, openai-responses",
194 cp.api,
195 cp.name
196 );
197 }
198 }
199
200 fetch_and_register_models(cp, &api, &api_key);
201 }
202}
203
204fn fetch_and_register_models(
206 cp: &crate::store::settings::CustomProvider,
207 api: &str,
208 api_key: &Option<String>,
209) {
210 if let Some(key) = api_key {
211 match oxi_sdk::fetch_models_blocking(&cp.base_url, key.as_str()) {
212 Ok(model_ids) => {
213 let count = model_ids.len();
214 for model_id in &model_ids {
215 let api_type = match api {
216 "openai-responses" | "responses" => oxi_sdk::Api::OpenAiResponses,
217 _ => oxi_sdk::Api::OpenAiCompletions,
218 };
219 let model = oxi_sdk::Model {
220 id: model_id.clone(),
221 name: model_id.clone(),
222 api: api_type,
223 provider: cp.name.clone(),
224 base_url: cp.base_url.clone(),
225 reasoning: false,
226 input: vec![oxi_sdk::InputModality::Text],
227 cost: oxi_sdk::Cost::default(),
228 context_window: 128_000,
229 max_tokens: 8_192,
230 headers: Default::default(),
231 compat: None,
232 };
233 oxi_sdk::register_model(model);
234 }
235 tracing::info!(
236 "[oxi] auto-fetched {} models from '{}' ({})",
237 count,
238 cp.name,
239 cp.base_url
240 );
241 }
242 Err(e) => {
243 tracing::warn!(
244 "[oxi] warning: failed to resolve models for {}: {}",
245 cp.name,
246 e
247 );
248 }
249 }
250 }
251}
252
253fn register_builtin_tools(
261 tools: &oxi_agent::ToolRegistry,
262 cwd: &std::path::Path,
263 args: &CliArgs,
264 disabled_tools: &[String],
265) {
266 let builtin_registry = if let Some(ref tools_str) = args.tools {
267 let names: Vec<&str> = tools_str.split(',').map(|s| s.trim()).collect();
268 oxi_agent::ToolRegistry::with_selected_tools(cwd.to_path_buf(), &names)
269 } else {
270 oxi_agent::ToolRegistry::with_builtins_cwd(cwd.to_path_buf(), disabled_tools)
271 };
272 for name in builtin_registry.names() {
273 if let Some(tool) = builtin_registry.get(&name) {
274 tools.register_arc(tool);
275 }
276 }
277 if let Some(mgr) = builtin_registry.mcp_manager() {
280 tools.set_mcp_manager(mgr);
281 }
282}
283
284fn load_wasm_extensions(
286 app: &crate::App,
287 cwd: &std::path::Path,
288 tools: &oxi_agent::ToolRegistry,
289) -> Option<std::sync::Arc<crate::extensions::WasmExtensionManager>> {
290 if !app.settings().extensions_enabled {
291 return None;
292 }
293
294 let wasm_paths = crate::extensions::WasmExtensionManager::discover(cwd);
295 if wasm_paths.is_empty() {
296 return None;
297 }
298
299 let mut wasm_mgr = crate::extensions::WasmExtensionManager::new();
300 let (loaded, errors) = wasm_mgr.load_all(&wasm_paths);
301 for info in &loaded {
302 tracing::info!("WASM extension loaded: {} v{}", info.name, info.version);
303 }
304 for err in &errors {
305 tracing::warn!("WASM extension error: {}", err);
306 }
307
308 if wasm_mgr.is_empty() {
309 return None;
310 }
311
312 let mgr = std::sync::Arc::new(wasm_mgr);
313 for tool_def in mgr.all_tool_defs() {
314 let wasm_tool = crate::extensions::WasmTool::new(
315 mgr.clone(),
316 tool_def.name.clone(),
317 tool_def.description.clone(),
318 tool_def.schema.clone(),
319 );
320 tools.register(wasm_tool);
321 }
322 Some(mgr)
323}
324
325fn register_router_provider(settings: &Settings) {
327 let global_dir = dirs::config_dir().unwrap_or_default().join("oxi");
328 let project_dir = std::env::current_dir().unwrap_or_default();
329
330 let store_cfg = match crate::store::router_config::load_router_config(&global_dir, &project_dir)
331 {
332 Some(cfg) => cfg,
333 None => {
334 tracing::debug!("No router config found — router/auto will not appear in model list");
335 return;
336 }
337 };
338
339 oxi_sdk::register_model(oxi_sdk::Model::new(
341 "auto",
342 "Router (auto)".to_string(),
343 oxi_sdk::Api::AnthropicMessages,
344 "router",
345 "router://local",
346 ));
347
348 let mut ai_profiles = std::collections::HashMap::new();
350 for (name, sp) in store_cfg.profiles() {
351 fn parse_thinking(s: &Option<String>) -> Option<oxi_sdk::ThinkingLevel> {
352 s.as_ref().and_then(|s| match s.as_str() {
353 "off" => Some(oxi_sdk::ThinkingLevel::Off),
354 "minimal" => Some(oxi_sdk::ThinkingLevel::Minimal),
355 "low" => Some(oxi_sdk::ThinkingLevel::Low),
356 "medium" => Some(oxi_sdk::ThinkingLevel::Medium),
357 "high" => Some(oxi_sdk::ThinkingLevel::High),
358 "xhigh" => Some(oxi_sdk::ThinkingLevel::XHigh),
359 _ => None,
360 })
361 }
362 ai_profiles.insert(
363 name.clone(),
364 oxi_sdk::router::RouterProfile {
365 high: oxi_sdk::router::RoutedTierConfig {
366 model: sp.high.model.clone(),
367 thinking: parse_thinking(&sp.high.thinking),
368 fallbacks: sp.high.fallbacks.clone(),
369 },
370 medium: oxi_sdk::router::RoutedTierConfig {
371 model: sp.medium.model.clone(),
372 thinking: parse_thinking(&sp.medium.thinking),
373 fallbacks: sp.medium.fallbacks.clone(),
374 },
375 low: oxi_sdk::router::RoutedTierConfig {
376 model: sp.low.model.clone(),
377 thinking: parse_thinking(&sp.low.thinking),
378 fallbacks: sp.low.fallbacks.clone(),
379 },
380 },
381 );
382 }
383 let ai_cfg = oxi_sdk::router::RouterConfig::with_pinning(
384 store_cfg.default_profile().to_string(),
385 store_cfg.classifier_model().map(String::from),
386 store_cfg.context_upgrade_threshold(),
387 store_cfg.max_session_budget(),
388 ai_profiles,
389 oxi_sdk::router::ScoringWeights {
390 structural: store_cfg.weights().structural,
391 behavioral: store_cfg.weights().behavioral,
392 context_budget: store_cfg.weights().context_budget,
393 vision: store_cfg.weights().vision,
394 message: store_cfg.weights().message,
395 },
396 store_cfg.pin_tier().and_then(|s| match s {
397 "high" => Some(oxi_sdk::router::RouterTier::High),
398 "medium" => Some(oxi_sdk::router::RouterTier::Medium),
399 "low" => Some(oxi_sdk::router::RouterTier::Low),
400 _ => None,
401 }),
402 store_cfg.phase_bias(),
403 );
404
405 oxi_sdk::router::register_router(&ai_cfg);
406
407 if let Some(profile) = settings.router_profile() {
408 tracing::info!("Router active with profile: {profile}");
409 }
410}