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