返回 DeepSeek-TUI-2026
provider.rs
根目录 / crates / tui / src / commands / provider.rs
1 //! Provider switching: flip between DeepSeek, hosted providers, and self-hosted
2 //! OpenAI-compatible DeepSeek V4 servers at runtime.
3 //!
4 //! `/provider` with no args opens the picker modal (#52). `/provider <name>`
5 //! keeps the v0.6.6 CLI form for muscle-memory + scripted use.
6
7 use crate::config::{ApiProvider, normalize_model_name};
8 use crate::tui::app::{App, AppAction};
9
10 use super::CommandResult;
11
12 /// Switch or view the current LLM backend.
13 ///
14 /// With no args, opens the picker modal. With `<provider> [model]`, performs
15 /// the switch directly (e.g. `/provider nim flash` lands on
16 /// `deepseek-ai/deepseek-v4-flash`). The optional model accepts shorthand
17 /// (`flash`, `pro`, `v4-flash`, `v4-pro`) or any normal DeepSeek model ID.
18 pub fn provider(app: &mut App, args: Option<&str>) -> CommandResult {
19 let trimmed = args.map(str::trim).filter(|s| !s.is_empty());
20 let Some(args) = trimmed else {
21 return CommandResult::action(AppAction::OpenProviderPicker);
22 };
23
24 let mut parts = args.split_whitespace();
25 let name = parts.next().unwrap_or("");
26 let model_arg = parts.next();
27
28 let Some(target) = ApiProvider::parse(name) else {
29 return CommandResult::error(format!(
30 "Unknown provider '{name}'. Expected: deepseek, nvidia-nim, openrouter, novita, fireworks, sglang, or vllm."
31 ));
32 };
33
34 let model = match model_arg {
35 None => None,
36 Some(raw) => match normalize_model_name(&expand_model_alias(raw)) {
37 Some(normalized) => Some(normalized),
38 None => {
39 return CommandResult::error(format!(
40 "Invalid model '{raw}'. Try: flash, pro, deepseek-v4-flash, deepseek-v4-pro."
41 ));
42 }
43 },
44 };
45
46 if target == app.api_provider && model.is_none() {
47 return CommandResult::message(format!("Already on provider: {}", target.as_str()));
48 }
49
50 CommandResult::action(AppAction::SwitchProvider {
51 provider: target,
52 model,
53 })
54 }
55
56 fn expand_model_alias(name: &str) -> String {
57 match name.trim().to_ascii_lowercase().as_str() {
58 "pro" | "v4-pro" => "deepseek-v4-pro".to_string(),
59 "flash" | "v4-flash" => "deepseek-v4-flash".to_string(),
60 other => other.to_string(),
61 }
62 }
63
64 #[cfg(test)]
65 mod tests {
66 use super::*;
67 use crate::config::Config;
68 use crate::tui::app::TuiOptions;
69 use std::path::PathBuf;
70
71 fn create_test_app() -> App {
72 let options = TuiOptions {
73 model: "deepseek-v4-pro".to_string(),
74 workspace: PathBuf::from("."),
75 config_path: None,
76 config_profile: None,
77 allow_shell: false,
78 use_alt_screen: true,
79 use_mouse_capture: false,
80 use_bracketed_paste: true,
81 max_subagents: 1,
82 skills_dir: PathBuf::from("."),
83 memory_path: PathBuf::from("memory.md"),
84 notes_path: PathBuf::from("notes.txt"),
85 mcp_config_path: PathBuf::from("mcp.json"),
86 use_memory: false,
87 start_in_agent_mode: false,
88 skip_onboarding: true,
89 yolo: false,
90 resume_session_id: None,
91 initial_input: None,
92 };
93 let mut app = App::new(options, &Config::default());
94 app.ui_locale = crate::localization::Locale::En;
95 app.api_provider = crate::config::ApiProvider::Deepseek;
96 app
97 }
98
99 #[test]
100 fn no_args_opens_picker_modal() {
101 let mut app = create_test_app();
102 let result = provider(&mut app, None);
103 assert!(result.message.is_none());
104 assert_eq!(result.action, Some(AppAction::OpenProviderPicker));
105 }
106
107 #[test]
108 fn unknown_provider_returns_error() {
109 let mut app = create_test_app();
110 let result = provider(&mut app, Some("anthropic"));
111 let msg = result.message.expect("expected error message");
112 assert!(msg.contains("Unknown provider"));
113 assert!(msg.contains("openrouter"));
114 assert!(msg.contains("novita"));
115 assert!(result.action.is_none());
116 }
117
118 #[test]
119 fn switch_to_openrouter_emits_action() {
120 let mut app = create_test_app();
121 let result = provider(&mut app, Some("openrouter"));
122 match result.action {
123 Some(AppAction::SwitchProvider { provider, model }) => {
124 assert_eq!(provider, ApiProvider::Openrouter);
125 assert_eq!(model, None);
126 }
127 other => panic!("expected SwitchProvider, got {other:?}"),
128 }
129 }
130
131 #[test]
132 fn switch_to_novita_emits_action() {
133 let mut app = create_test_app();
134 let result = provider(&mut app, Some("novita"));
135 match result.action {
136 Some(AppAction::SwitchProvider { provider, model }) => {
137 assert_eq!(provider, ApiProvider::Novita);
138 assert_eq!(model, None);
139 }
140 other => panic!("expected SwitchProvider, got {other:?}"),
141 }
142 }
143
144 #[test]
145 fn switch_to_fireworks_emits_action() {
146 let mut app = create_test_app();
147 let result = provider(&mut app, Some("fireworks pro"));
148 match result.action {
149 Some(AppAction::SwitchProvider { provider, model }) => {
150 assert_eq!(provider, ApiProvider::Fireworks);
151 assert_eq!(model.as_deref(), Some("deepseek-v4-pro"));
152 }
153 other => panic!("expected SwitchProvider, got {other:?}"),
154 }
155 }
156
157 #[test]
158 fn switch_to_sglang_flash_emits_action() {
159 let mut app = create_test_app();
160 let result = provider(&mut app, Some("sglang flash"));
161 match result.action {
162 Some(AppAction::SwitchProvider { provider, model }) => {
163 assert_eq!(provider, ApiProvider::Sglang);
164 assert_eq!(model.as_deref(), Some("deepseek-v4-flash"));
165 }
166 other => panic!("expected SwitchProvider, got {other:?}"),
167 }
168 }
169
170 #[test]
171 fn switch_to_vllm_flash_emits_action() {
172 let mut app = create_test_app();
173 let result = provider(&mut app, Some("vllm flash"));
174 match result.action {
175 Some(AppAction::SwitchProvider { provider, model }) => {
176 assert_eq!(provider, ApiProvider::Vllm);
177 assert_eq!(model.as_deref(), Some("deepseek-v4-flash"));
178 }
179 other => panic!("expected SwitchProvider, got {other:?}"),
180 }
181 }
182
183 #[test]
184 fn switching_to_active_provider_without_model_is_a_noop() {
185 let mut app = create_test_app();
186 let result = provider(&mut app, Some("deepseek"));
187 let msg = result.message.expect("expected message");
188 assert!(msg.contains("Already on provider"));
189 assert!(result.action.is_none());
190 }
191
192 #[test]
193 fn switch_to_nim_emits_action_without_model_override() {
194 let mut app = create_test_app();
195 let result = provider(&mut app, Some("nvidia-nim"));
196 assert!(result.message.is_none());
197 match result.action {
198 Some(AppAction::SwitchProvider { provider, model }) => {
199 assert_eq!(provider, ApiProvider::NvidiaNim);
200 assert_eq!(model, None);
201 }
202 other => panic!("expected SwitchProvider action, got {other:?}"),
203 }
204 }
205
206 #[test]
207 fn nim_flash_shorthand_emits_action_with_model_override() {
208 let mut app = create_test_app();
209 let result = provider(&mut app, Some("nim flash"));
210 match result.action {
211 Some(AppAction::SwitchProvider { provider, model }) => {
212 assert_eq!(provider, ApiProvider::NvidiaNim);
213 assert_eq!(model.as_deref(), Some("deepseek-v4-flash"));
214 }
215 other => panic!("expected SwitchProvider action, got {other:?}"),
216 }
217 }
218
219 #[test]
220 fn nim_pro_shorthand_emits_action_with_model_override() {
221 let mut app = create_test_app();
222 let result = provider(&mut app, Some("nim pro"));
223 match result.action {
224 Some(AppAction::SwitchProvider { provider, model }) => {
225 assert_eq!(provider, ApiProvider::NvidiaNim);
226 assert_eq!(model.as_deref(), Some("deepseek-v4-pro"));
227 }
228 other => panic!("expected SwitchProvider action, got {other:?}"),
229 }
230 }
231
232 #[test]
233 fn switch_to_active_provider_with_new_model_still_emits_action() {
234 let mut app = create_test_app();
235 let result = provider(&mut app, Some("deepseek flash"));
236 match result.action {
237 Some(AppAction::SwitchProvider { provider, model }) => {
238 assert_eq!(provider, ApiProvider::Deepseek);
239 assert_eq!(model.as_deref(), Some("deepseek-v4-flash"));
240 }
241 other => panic!("expected SwitchProvider action, got {other:?}"),
242 }
243 }
244
245 #[test]
246 fn invalid_model_returns_error() {
247 let mut app = create_test_app();
248 let result = provider(&mut app, Some("nim gpt-4"));
249 let msg = result.message.expect("expected error message");
250 assert!(msg.contains("Invalid model"));
251 assert!(result.action.is_none());
252 }
253 }
254
254 lines RUST