CLI update: subcommand must be specified

This commit is contained in:
jhqxxx
2026-04-03 20:43:51 +08:00
parent 279480e3d7
commit 44d91da650
17 changed files with 240 additions and 212 deletions
+40 -73
View File
@@ -5,40 +5,8 @@ use clap::{Args, Parser, Subcommand};
#[command(name = "aha")]
#[command(version, about, long_about = None)]
pub(crate) struct Cli {
/// Service listen address
#[arg(short, long, default_value = "127.0.0.1")]
pub address: Option<String>,
/// Service listen port
#[arg(short, long)]
pub port: Option<u16>,
/// Model type (required for backward compatibility)
#[arg(short, long)]
pub model: Option<WhichModel>,
/// Local model weight path
#[arg(long)]
pub weight_path: Option<String>,
/// Model download save directory
#[arg(long)]
pub save_dir: Option<String>,
/// Download retry count
#[arg(long)]
pub download_retries: Option<u32>,
/// Local GGUF model weight path (required for loading models with GGUF).
#[arg(long)]
pub gguf_path: Option<String>,
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
#[arg(long)]
pub mmproj_path: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
pub command: Commands,
}
#[derive(Subcommand, Debug)]
@@ -61,7 +29,7 @@ pub(crate) enum Commands {
/// Common/shared arguments for server operations
#[derive(Args, Debug)]
pub(crate) struct CommonArgs {
pub(crate) struct ServerCommonArgs {
/// Service listen address
#[arg(short, long, default_value = "127.0.0.1")]
pub address: String,
@@ -70,24 +38,42 @@ pub(crate) struct CommonArgs {
#[arg(short, long, default_value_t = 10100)]
pub port: u16,
/// Model type (required)
#[arg(short, long)]
pub model: WhichModel,
/// Allow remote shutdown requests (default: local only, use with caution)
#[arg(long)]
pub allow_remote_shutdown: bool,
}
/// Arguments for the 'cli' subcommand (download + serve)
#[derive(Args, Debug)]
pub(crate) struct CliArgs {
#[command(flatten)]
pub common: CommonArgs,
pub(crate) struct PathCommonArgs {
/// Local model weight path (skip download if provided)
#[arg(long)]
pub weight_path: Option<String>,
/// Local GGUF model weight path (required for loading models with GGUF).
#[arg(long)]
pub gguf_path: Option<String>,
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
#[arg(long)]
pub mmproj_path: Option<String>,
/// Local path for onnx model weights (required for loading with onnx)
#[arg(long)]
pub onnx_path: Option<String>,
/// config path for onnx/gguf model need extra config file
#[arg(long)]
pub config_path: Option<String>,
}
/// Arguments for the 'cli' subcommand (download + serve)
#[derive(Args, Debug)]
pub(crate) struct CliArgs {
/// Model type (required)
#[arg(short, long)]
pub model: WhichModel,
#[command(flatten)]
pub server_common: ServerCommonArgs,
/// Model download save directory
#[arg(long)]
@@ -97,32 +83,22 @@ pub(crate) struct CliArgs {
#[arg(long)]
pub download_retries: Option<u32>,
/// Local GGUF model weight path (required for loading models with GGUF).
#[arg(long)]
pub gguf_path: Option<String>,
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
#[arg(long)]
pub mmproj_path: Option<String>,
#[command(flatten)]
pub path_common: PathCommonArgs,
}
/// Arguments for the 'serv start' subcommand
#[derive(Args, Debug)]
pub(crate) struct ServArgs {
/// Model type (required)
#[arg(short, long)]
pub model: WhichModel,
#[command(flatten)]
pub common: CommonArgs,
pub server_common: ServerCommonArgs,
/// Local model weight path (defaults to ~/.aha/{model_id} if not specified)
#[arg(long)]
pub weight_path: Option<String>,
/// Local GGUF model weight path (required for loading models with GGUF).
#[arg(long)]
pub gguf_path: Option<String>,
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
#[arg(long)]
pub mmproj_path: Option<String>,
#[command(flatten)]
pub path_common: PathCommonArgs,
}
/// Arguments for the 'serv list' subcommand
@@ -164,17 +140,8 @@ pub(crate) struct RunArgs {
#[arg(short, long)]
pub output: Option<String>,
/// Local model weight path (defaults to ~/.aha/{model_id} if not specified)
#[arg(long)]
pub weight_path: Option<String>,
/// Local GGUF model weight path (required for loading models with GGUF).
#[arg(long)]
pub gguf_path: Option<String>,
/// Local path for mmproj GGUF model weights (required for loading with multimodel GGUF)
#[arg(long)]
pub mmproj_path: Option<String>,
#[command(flatten)]
pub path_common: PathCommonArgs,
}
/// Arguments for the 'delete' subcommand (delete model from default location)
+58 -28
View File
@@ -76,22 +76,27 @@ pub(crate) fn run_list(args: ListArgs) -> anyhow::Result<()> {
/// Run the 'cli' subcommand: download model (if needed) and start service
pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
let CliArgs {
common,
weight_path,
model,
server_common,
save_dir,
download_retries,
gguf_path,
mmproj_path,
path_common,
} = args;
let model_id = common.model.as_string();
let model_id = model.as_string();
let (model_path, gguf, mmproj) = if model_id.contains("gguf") {
if gguf_path.is_none() {
let (model_path, gguf, mmproj) = if model.is_gguf() {
if path_common.gguf_path.is_none() {
return Err(anyhow!("gguf model path is required"));
}
("GGUF".to_string(), gguf_path, mmproj_path)
(
"GGUF".to_string(),
path_common.gguf_path,
path_common.mmproj_path,
)
} else if model.is_onnx() {
return Err(anyhow!("onnx model not support now"));
} else {
let model_path = match weight_path {
let model_path = match path_common.weight_path {
Some(path) => path,
None => {
let save_dir = match save_dir {
@@ -106,8 +111,13 @@ pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
(model_path, None, None)
};
init(common.model, model_path, gguf, mmproj)?;
start_http_server(common.address, common.port, common.allow_remote_shutdown).await?;
init(model, model_path, gguf, mmproj)?;
start_http_server(
server_common.address,
server_common.port,
server_common.allow_remote_shutdown,
)
.await?;
Ok(())
}
@@ -115,27 +125,41 @@ pub(crate) async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
/// Run the 'serv' subcommand: start service only (no download)
pub(crate) async fn run_serv(args: ServArgs) -> anyhow::Result<()> {
let ServArgs {
common,
weight_path,
gguf_path,
mmproj_path,
model,
server_common,
path_common,
} = args;
let model_id = common.model.as_string();
let (model_path, gguf, mmproj) = if model_id.contains("gguf") {
if gguf_path.is_none() {
let (model_path, gguf, mmproj) = if model.is_gguf() {
if path_common.gguf_path.is_none() {
return Err(anyhow!("gguf model path is required"));
}
("GGUF".to_string(), gguf_path, mmproj_path)
(
"GGUF".to_string(),
path_common.gguf_path,
path_common.mmproj_path,
)
} else if model.is_onnx() {
return Err(anyhow!("onnx model not support now"));
} else {
let model_path = match weight_path {
let model_path = match path_common.weight_path {
Some(path) => path,
None => get_default_weight_path(common.model),
None => get_default_weight_path(model),
};
if !std::path::Path::new(&model_path).exists() {
return Err(anyhow!(
"serv subcommand will not download model, use `weight-path` to pass the model path"
));
}
(model_path, None, None)
};
init(common.model, model_path, gguf, mmproj)?;
start_http_server(common.address, common.port, common.allow_remote_shutdown).await?;
init(model, model_path, gguf, mmproj)?;
start_http_server(
server_common.address,
server_common.port,
server_common.allow_remote_shutdown,
)
.await?;
Ok(())
}
@@ -205,13 +229,11 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
model,
input,
output,
weight_path,
gguf_path,
mmproj_path,
path_common,
} = args;
// Use default weight path if not specified
let weight_path = match weight_path {
let weight_path = match path_common.weight_path {
Some(path) => path,
None => get_default_weight_path(model),
};
@@ -243,6 +265,9 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
WhichModel::Qwen3_1_7B => {
qwen3::Qwen3Exec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::Qwen3_4B => {
qwen3::Qwen3Exec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::Qwen3_5_0_8B => {
qwen3_5::Qwen3_5Exec::run(&input, output.as_deref(), &weight_path)?;
}
@@ -256,7 +281,12 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
qwen3_5::Qwen3_5Exec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::Qwen3_5Gguf => {
qwen3_5::Qwen3_5Exec::run_gguf(&input, output.as_deref(), gguf_path, mmproj_path)?;
qwen3_5::Qwen3_5Exec::run_gguf(
&input,
output.as_deref(),
path_common.gguf_path,
path_common.mmproj_path,
)?;
}
WhichModel::Qwen3ASR0_6B => {
qwen3_asr::Qwen3ASRExec::run(&input, output.as_deref(), &weight_path)?;
+8 -27
View File
@@ -1,7 +1,7 @@
use clap::Parser;
use crate::cli::{
args::{Cli, CliArgs, Commands, CommonArgs},
args::{Cli, Commands},
run_cli, run_delete, run_download, run_list, run_ps, run_run, run_serv,
};
@@ -11,32 +11,13 @@ mod server;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Cli(args)) => run_cli(args).await,
Some(Commands::Serv(args)) => run_serv(args).await,
Some(Commands::Ps(args)) => run_ps(args),
Some(Commands::Delete(args)) => run_delete(args),
Some(Commands::Download(args)) => run_download(args).await,
Some(Commands::Run(args)) => run_run(args),
Some(Commands::List(args)) => run_list(args),
None => {
// Backward compatibility: when no subcommand is provided, use 'cli' behavior
let model = cli.model.expect("Model is required (use -m or --model)");
let args = CliArgs {
common: CommonArgs {
address: cli.address.unwrap_or_else(|| "127.0.0.1".to_string()),
port: cli.port.unwrap_or(10100),
model,
allow_remote_shutdown: false,
},
weight_path: cli.weight_path,
save_dir: cli.save_dir,
download_retries: cli.download_retries,
gguf_path: cli.gguf_path,
mmproj_path: cli.mmproj_path,
};
run_cli(args).await
}
Commands::Cli(args) => run_cli(args).await,
Commands::Serv(args) => run_serv(args).await,
Commands::Ps(args) => run_ps(args),
Commands::Delete(args) => run_delete(args),
Commands::Download(args) => run_download(args).await,
Commands::Run(args) => run_run(args),
Commands::List(args) => run_list(args),
}
}
+26 -1
View File
@@ -20,6 +20,8 @@ pub enum WhichModel {
Qwen3_0_6B,
#[value(name = "Qwen/Qwen3-1.7B")]
Qwen3_1_7B,
#[value(name = "Qwen/Qwen3-4B")]
Qwen3_4B,
#[value(name = "Qwen/Qwen3.5-0.8B")]
Qwen3_5_0_8B,
#[value(name = "Qwen/Qwen3.5-2B")]
@@ -67,18 +69,40 @@ pub enum WhichModel {
}
impl WhichModel {
/// Get the ModelScope model ID for this model variant
/// Get the model ID for this model variant
pub fn as_string(&self) -> String {
self.to_possible_value()
.expect("not exists")
.get_name()
.to_string()
}
/// Checks if the model is in GGUF format
///
/// Returns true if the model ID contains "gguf", false otherwise
pub fn is_gguf(&self) -> bool {
let model_id = self.as_string();
model_id.to_lowercase().contains("gguf")
}
/// Checks if the model is in ONNX format
///
/// Returns true if the model ID contains "onnx", false otherwise
pub fn is_onnx(&self) -> bool {
let model_id = self.as_string();
model_id.to_lowercase().contains("onnx")
}
/// Get the WhichModel enum list
pub fn model_list() -> Vec<Self> {
WhichModel::value_variants().to_vec()
}
/// Extracts the model owner/organization from the model ID
///
/// Splits the model ID string on '/' and returns the first part which typically represents
/// the organization or user who owns the model in Hugging Face format (e.g., "Qwen" from "Qwen/Qwen3-0.6B")
/// Returns "none" if the model ID doesn't contain a '/' separator
pub fn model_owner(&self) -> String {
let name = self.as_string();
let names: Vec<&str> = name.split("/").collect();
@@ -96,6 +120,7 @@ impl WhichModel {
WhichModel::MiniCPM4_0_5B
| WhichModel::Qwen3_0_6B
| WhichModel::Qwen3_1_7B
| WhichModel::Qwen3_4B
| WhichModel::LFM2_1_2B
| WhichModel::LFM2_5_1_2BInstruct => "llm",
// VLM models
+27 -12
View File
@@ -129,12 +129,27 @@ impl<'a> GenerateModel for ModelInstance<'a> {
}
}
pub fn load_model<'a>(
#[allow(unused)]
pub fn load_gguf_model<'a>(
model_type: WhichModel,
path: &str,
gguf: Option<&str>,
mmproj: Option<&str>,
config_path: Option<&str>, // 有些gguf未包含模型其他配置,需额外指定
gguf_path: &str,
mmproj_path: Option<&str>,
) -> Result<ModelInstance<'a>> {
let model = match model_type {
WhichModel::Qwen3_5Gguf => {
let model = Qwen3_5GenerateModel::init_from_gguf(gguf_path, mmproj_path, None)?;
ModelInstance::Qwen3_5(model)
}
_ => {
let model_id = model_type.as_string();
return Err(anyhow!("model id {model_id} is not gguf model"));
}
};
Ok(model)
}
pub fn load_model<'a>(model_type: WhichModel, path: &str) -> Result<ModelInstance<'a>> {
let model = match model_type {
WhichModel::MiniCPM4_0_5B => {
let model = MiniCPMGenerateModel::init(path, None, None)?;
@@ -172,6 +187,10 @@ pub fn load_model<'a>(
let model = Qwen3GenerateModel::init(path, None, None)?;
ModelInstance::Qwen3(model)
}
WhichModel::Qwen3_4B => {
let model = Qwen3GenerateModel::init(path, None, None)?;
ModelInstance::Qwen3(model)
}
WhichModel::Qwen3_5_0_8B => {
let model = Qwen3_5GenerateModel::init(path, None, None)?;
ModelInstance::Qwen3_5(model)
@@ -188,14 +207,6 @@ pub fn load_model<'a>(
let model = Qwen3_5GenerateModel::init(path, None, None)?;
ModelInstance::Qwen3_5(model)
}
WhichModel::Qwen3_5Gguf => {
if gguf.is_none() {
return Err(anyhow!("Qwen3_5Gguf gguf model path is required"));
}
let gguf = gguf.unwrap();
let model = Qwen3_5GenerateModel::init_from_gguf(gguf, mmproj, None)?;
ModelInstance::Qwen3_5(model)
}
WhichModel::Qwen3ASR0_6B => {
let model = Qwen3AsrGenerateModel::init(path, None, None)?;
ModelInstance::Qwen3ASR(model)
@@ -264,6 +275,10 @@ pub fn load_model<'a>(
let model = GlmOcrGenerateModel::init(path, None, None)?;
ModelInstance::GlmOCR(model)
}
_ => {
let model_id = model_type.as_string();
return Err(anyhow!("model id {model_id} is not safetensor model"));
}
};
Ok(model)
}
+2 -2
View File
@@ -59,7 +59,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
qwen3_5,
device,
model_name: model_name.to_string(),
repeat_penalty: 1.01,
repeat_penalty: 1.0,
repeat_last_n: 64,
})
}
@@ -116,7 +116,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
qwen3_5,
device,
model_name: stem.to_string(),
repeat_penalty: 1.1,
repeat_penalty: 1.2,
repeat_last_n: 64,
})
}
+17 -4
View File
@@ -2,9 +2,11 @@ use std::pin::pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use aha::models::load_gguf_model;
use aha::models::{GenerateModel, ModelInstance, common::model_mapping::WhichModel, load_model};
use aha::params::chat::ChatCompletionParameters;
use aha::utils::string_to_static_str;
use anyhow::anyhow;
use rocket::futures::StreamExt;
use rocket::serde::{Serialize, json::Json};
use rocket::{
@@ -37,10 +39,21 @@ pub fn init(
gguf: Option<String>,
mmproj: Option<String>,
) -> anyhow::Result<()> {
let model_path = string_to_static_str(path);
let gguf = gguf.map(string_to_static_str);
let mmproj = mmproj.map(string_to_static_str);
let model = load_model(model_type, model_path, gguf, mmproj)?;
let model = if model_type.is_gguf() {
if let Some(gguf_path) = gguf {
let gguf_path = string_to_static_str(gguf_path);
let mmproj_path = mmproj.map(string_to_static_str);
load_gguf_model(model_type, None, gguf_path, mmproj_path)?
} else {
return Err(anyhow!("gguf model need gguf model path"));
}
} else if model_type.is_onnx() {
return Err(anyhow!("onnx comming soon but now not support"));
} else {
let model_path = string_to_static_str(path);
load_model(model_type, model_path)?
};
MODEL.get_or_init(|| {
Arc::new(RwLock::new(StoredModel {
which_model: model_type,