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