feat: DeepSeek V4 support with reasoning-effort control (0.4.0)
Adds first-class DeepSeek V4 Pro and Flash support, updates the default model to deepseek-v4-pro, aligns legacy aliases with the current V4 1M context behavior, and fixes thinking-mode request handling. Key fixes: - Send DeepSeek's raw Chat Completions `thinking` parameter at the top level instead of SDK-only `extra_body`. - Preserve assistant `reasoning_content` for all prior thinking-mode tool-call turns so subsequent requests satisfy DeepSeek V4's replay requirement. - Fix npm wrapper concurrent first-run downloads by using per-process temporary download paths. - Add `.mailmap` so historical bot-attributed commits aggregate under Hunter Bown where mailmap is honored. Verified with the full local Rust gate, live DeepSeek V4 smoke, npm wrapper temp-install smoke, and green PR CI across Linux, macOS, and Windows.
This commit is contained in:
+215
-12
@@ -10,7 +10,7 @@ use deepseek_agent::ModelRegistry;
|
||||
use deepseek_app_server::{
|
||||
AppServerOptions, run as run_app_server, run_stdio as run_app_server_stdio,
|
||||
};
|
||||
use deepseek_config::{CliRuntimeOverrides, ConfigStore, ProviderKind};
|
||||
use deepseek_config::{CliRuntimeOverrides, ConfigStore, ProviderKind, ResolvedRuntimeOptions};
|
||||
use deepseek_execpolicy::{AskForApproval, ExecPolicyContext, ExecPolicyEngine};
|
||||
use deepseek_mcp::{McpServerDefinition, run_stdio_server};
|
||||
use deepseek_state::{StateStore, ThreadListFilters};
|
||||
@@ -42,7 +42,11 @@ struct Cli {
|
||||
config: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
profile: Option<String>,
|
||||
#[arg(long, value_enum)]
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
help = "Advanced provider selector for non-TUI registry/config commands"
|
||||
)]
|
||||
provider: Option<ProviderArg>,
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
@@ -70,7 +74,37 @@ struct Cli {
|
||||
enum Commands {
|
||||
/// Run interactive/non-interactive flows via the TUI binary.
|
||||
Run(RunArgs),
|
||||
/// Login using API key, ChatGPT token, or device code style session.
|
||||
/// Run DeepSeek TUI diagnostics.
|
||||
Doctor(TuiPassthroughArgs),
|
||||
/// List live DeepSeek API models via the TUI binary.
|
||||
Models(TuiPassthroughArgs),
|
||||
/// List saved TUI sessions.
|
||||
Sessions(TuiPassthroughArgs),
|
||||
/// Resume a saved TUI session.
|
||||
Resume(TuiPassthroughArgs),
|
||||
/// Fork a saved TUI session.
|
||||
Fork(TuiPassthroughArgs),
|
||||
/// Create a default AGENTS.md in the current directory.
|
||||
Init(TuiPassthroughArgs),
|
||||
/// Bootstrap MCP config and/or skills directories.
|
||||
Setup(TuiPassthroughArgs),
|
||||
/// Run the DeepSeek TUI non-interactive agent command.
|
||||
Exec(TuiPassthroughArgs),
|
||||
/// Run a DeepSeek-powered code review over a git diff.
|
||||
Review(TuiPassthroughArgs),
|
||||
/// Apply a patch file or stdin to the working tree.
|
||||
Apply(TuiPassthroughArgs),
|
||||
/// Run the offline TUI evaluation harness.
|
||||
Eval(TuiPassthroughArgs),
|
||||
/// Manage TUI MCP servers.
|
||||
Mcp(TuiPassthroughArgs),
|
||||
/// Inspect TUI feature flags.
|
||||
Features(TuiPassthroughArgs),
|
||||
/// Run a local TUI server.
|
||||
Serve(TuiPassthroughArgs),
|
||||
/// Generate shell completions for the TUI binary.
|
||||
Completions(TuiPassthroughArgs),
|
||||
/// Save a DeepSeek API key to the shared config.
|
||||
Login(LoginArgs),
|
||||
/// Remove saved authentication state.
|
||||
Logout,
|
||||
@@ -101,17 +135,23 @@ struct RunArgs {
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args, Clone)]
|
||||
struct TuiPassthroughArgs {
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct LoginArgs {
|
||||
#[arg(long, value_enum, default_value_t = ProviderArg::Deepseek)]
|
||||
#[arg(long, value_enum, default_value_t = ProviderArg::Deepseek, hide = true)]
|
||||
provider: ProviderArg,
|
||||
#[arg(long)]
|
||||
api_key: Option<String>,
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(long, default_value_t = false, hide = true)]
|
||||
chatgpt: bool,
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(long, default_value_t = false, hide = true)]
|
||||
device_code: bool,
|
||||
#[arg(long)]
|
||||
#[arg(long, hide = true)]
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
@@ -279,12 +319,57 @@ fn run() -> Result<()> {
|
||||
approval_policy: cli.approval_policy.clone(),
|
||||
sandbox_mode: cli.sandbox_mode.clone(),
|
||||
};
|
||||
let _resolved_runtime = store.config.resolve_runtime_options(&runtime_overrides);
|
||||
let resolved_runtime = store.config.resolve_runtime_options(&runtime_overrides);
|
||||
|
||||
let command = cli.command.take();
|
||||
|
||||
match command {
|
||||
Some(Commands::Run(args)) => delegate_to_tui(&cli, args.args),
|
||||
Some(Commands::Run(args)) => delegate_to_tui(&cli, &resolved_runtime, args.args),
|
||||
Some(Commands::Doctor(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("doctor", args))
|
||||
}
|
||||
Some(Commands::Models(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("models", args))
|
||||
}
|
||||
Some(Commands::Sessions(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("sessions", args))
|
||||
}
|
||||
Some(Commands::Resume(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("resume", args))
|
||||
}
|
||||
Some(Commands::Fork(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("fork", args))
|
||||
}
|
||||
Some(Commands::Init(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("init", args))
|
||||
}
|
||||
Some(Commands::Setup(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("setup", args))
|
||||
}
|
||||
Some(Commands::Exec(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("exec", args))
|
||||
}
|
||||
Some(Commands::Review(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("review", args))
|
||||
}
|
||||
Some(Commands::Apply(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("apply", args))
|
||||
}
|
||||
Some(Commands::Eval(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("eval", args))
|
||||
}
|
||||
Some(Commands::Mcp(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("mcp", args))
|
||||
}
|
||||
Some(Commands::Features(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("features", args))
|
||||
}
|
||||
Some(Commands::Serve(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("serve", args))
|
||||
}
|
||||
Some(Commands::Completions(args)) => {
|
||||
delegate_to_tui(&cli, &resolved_runtime, tui_args("completions", args))
|
||||
}
|
||||
Some(Commands::Login(args)) => run_login_command(&mut store, args),
|
||||
Some(Commands::Logout) => run_logout_command(&mut store),
|
||||
Some(Commands::Auth(args)) => run_auth_command(&mut store, args.command),
|
||||
@@ -305,11 +390,18 @@ fn run() -> Result<()> {
|
||||
forwarded.push("--prompt".to_string());
|
||||
forwarded.push(prompt);
|
||||
}
|
||||
delegate_to_tui(&cli, forwarded)
|
||||
delegate_to_tui(&cli, &resolved_runtime, forwarded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tui_args(command: &str, args: TuiPassthroughArgs) -> Vec<String> {
|
||||
let mut forwarded = Vec::with_capacity(args.args.len() + 1);
|
||||
forwarded.push(command.to_string());
|
||||
forwarded.extend(args.args);
|
||||
forwarded
|
||||
}
|
||||
|
||||
fn run_login_command(store: &mut ConfigStore, args: LoginArgs) -> Result<()> {
|
||||
let provider: ProviderKind = args.provider.into();
|
||||
store.config.provider = provider;
|
||||
@@ -349,12 +441,33 @@ fn run_login_command(store: &mut ConfigStore, args: LoginArgs) -> Result<()> {
|
||||
};
|
||||
store.config.auth_mode = Some("api_key".to_string());
|
||||
store.config.providers.for_provider_mut(provider).api_key = Some(api_key);
|
||||
if provider == ProviderKind::Deepseek {
|
||||
store.config.api_key = store.config.providers.deepseek.api_key.clone();
|
||||
if store.config.default_text_model.is_none() {
|
||||
store.config.default_text_model = Some(
|
||||
store
|
||||
.config
|
||||
.providers
|
||||
.deepseek
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| "deepseek-v4-pro".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
store.save()?;
|
||||
println!("logged in using API key mode ({})", provider.as_str());
|
||||
if provider == ProviderKind::Deepseek {
|
||||
println!(
|
||||
"logged in using API key mode (deepseek). This also updates the shared deepseek-tui config."
|
||||
);
|
||||
} else {
|
||||
println!("logged in using API key mode ({})", provider.as_str());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_logout_command(store: &mut ConfigStore) -> Result<()> {
|
||||
store.config.api_key = None;
|
||||
store.config.providers.deepseek.api_key = None;
|
||||
store.config.providers.openai.api_key = None;
|
||||
store.config.auth_mode = None;
|
||||
@@ -382,6 +495,7 @@ fn run_auth_command(store: &mut ConfigStore, command: AuthCommand) -> Result<()>
|
||||
.deepseek
|
||||
.api_key
|
||||
.as_ref()
|
||||
.or(store.config.api_key.as_ref())
|
||||
.is_some_and(|v| !v.trim().is_empty());
|
||||
let openai_file = store
|
||||
.config
|
||||
@@ -407,6 +521,9 @@ fn run_auth_command(store: &mut ConfigStore, command: AuthCommand) -> Result<()>
|
||||
};
|
||||
store.config.provider = provider;
|
||||
store.config.providers.for_provider_mut(provider).api_key = Some(api_key);
|
||||
if provider == ProviderKind::Deepseek {
|
||||
store.config.api_key = store.config.providers.deepseek.api_key.clone();
|
||||
}
|
||||
store.save()?;
|
||||
println!("saved API key for {}", provider.as_str());
|
||||
Ok(())
|
||||
@@ -414,6 +531,9 @@ fn run_auth_command(store: &mut ConfigStore, command: AuthCommand) -> Result<()>
|
||||
AuthCommand::Clear { provider } => {
|
||||
let provider: ProviderKind = provider.into();
|
||||
store.config.providers.for_provider_mut(provider).api_key = None;
|
||||
if provider == ProviderKind::Deepseek {
|
||||
store.config.api_key = None;
|
||||
}
|
||||
store.save()?;
|
||||
println!("cleared API key for {}", provider.as_str());
|
||||
Ok(())
|
||||
@@ -623,7 +743,11 @@ fn persist_mcp_server_definitions(
|
||||
store.save()
|
||||
}
|
||||
|
||||
fn delegate_to_tui(cli: &Cli, passthrough: Vec<String>) -> Result<()> {
|
||||
fn delegate_to_tui(
|
||||
cli: &Cli,
|
||||
resolved_runtime: &ResolvedRuntimeOptions,
|
||||
passthrough: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let current = std::env::current_exe().context("failed to locate current executable path")?;
|
||||
let tui = current.with_file_name("deepseek-tui");
|
||||
if !tui.exists() {
|
||||
@@ -642,6 +766,19 @@ fn delegate_to_tui(cli: &Cli, passthrough: Vec<String>) -> Result<()> {
|
||||
}
|
||||
cmd.args(passthrough);
|
||||
|
||||
if resolved_runtime.provider != ProviderKind::Deepseek {
|
||||
bail!(
|
||||
"The interactive TUI only supports the DeepSeek API. Remove --provider {} or use `deepseek model ...` for provider registry inspection.",
|
||||
resolved_runtime.provider.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
cmd.env("DEEPSEEK_MODEL", &resolved_runtime.model);
|
||||
cmd.env("DEEPSEEK_BASE_URL", &resolved_runtime.base_url);
|
||||
if let Some(api_key) = resolved_runtime.api_key.as_ref() {
|
||||
cmd.env("DEEPSEEK_API_KEY", api_key);
|
||||
}
|
||||
|
||||
if let Some(provider) = cli.provider {
|
||||
cmd.env("DEEPSEEK_PROVIDER", ProviderKind::from(provider).as_str());
|
||||
}
|
||||
@@ -931,6 +1068,67 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_direct_tui_command_aliases() {
|
||||
let cli = parse_ok(&["deepseek", "doctor"]);
|
||||
assert!(matches!(
|
||||
cli.command,
|
||||
Some(Commands::Doctor(TuiPassthroughArgs { ref args })) if args.is_empty()
|
||||
));
|
||||
|
||||
let cli = parse_ok(&["deepseek", "models", "--json"]);
|
||||
assert!(matches!(
|
||||
cli.command,
|
||||
Some(Commands::Models(TuiPassthroughArgs { ref args })) if args == &["--json"]
|
||||
));
|
||||
|
||||
let cli = parse_ok(&["deepseek", "resume", "abc123"]);
|
||||
assert!(matches!(
|
||||
cli.command,
|
||||
Some(Commands::Resume(TuiPassthroughArgs { ref args })) if args == &["abc123"]
|
||||
));
|
||||
|
||||
let cli = parse_ok(&["deepseek", "setup", "--skills", "--local"]);
|
||||
assert!(matches!(
|
||||
cli.command,
|
||||
Some(Commands::Setup(TuiPassthroughArgs { ref args }))
|
||||
if args == &["--skills", "--local"]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_login_writes_tui_compatible_config() {
|
||||
let nanos = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"deepseek-cli-login-test-{}-{nanos}.toml",
|
||||
std::process::id()
|
||||
));
|
||||
let mut store = ConfigStore::load(Some(path.clone())).expect("store should load");
|
||||
|
||||
run_login_command(
|
||||
&mut store,
|
||||
LoginArgs {
|
||||
provider: ProviderArg::Deepseek,
|
||||
api_key: Some("sk-test".to_string()),
|
||||
chatgpt: false,
|
||||
device_code: false,
|
||||
token: None,
|
||||
},
|
||||
)
|
||||
.expect("login should write config");
|
||||
|
||||
assert_eq!(store.config.api_key.as_deref(), Some("sk-test"));
|
||||
assert_eq!(
|
||||
store.config.default_text_model.as_deref(),
|
||||
Some("deepseek-v4-pro")
|
||||
);
|
||||
let saved = std::fs::read_to_string(&path).expect("config should be written");
|
||||
assert!(saved.contains("api_key = \"sk-test\""));
|
||||
assert!(saved.contains("default_text_model = \"deepseek-v4-pro\""));
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_global_override_flags() {
|
||||
let cli = parse_ok(&[
|
||||
@@ -981,6 +1179,11 @@ mod tests {
|
||||
|
||||
for token in [
|
||||
"run",
|
||||
"doctor",
|
||||
"models",
|
||||
"sessions",
|
||||
"resume",
|
||||
"setup",
|
||||
"login",
|
||||
"logout",
|
||||
"auth",
|
||||
|
||||
Reference in New Issue
Block a user