From 6d51ce9504dce6cb461e4a6190fbb3301d22c646 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 19 Aug 2026 21:05:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20v0.7.0=20=E2=80=94=20am=20run/start=20-?= =?UTF-8?q?-provider/--model=20:=20r=C3=A9solution=20cloud=20via=20le=20re?= =?UTF-8?q?gistre=20(base=5Furl=20+=20keyring=20+=20mod=C3=A8le),=20fallba?= =?UTF-8?q?ck=20cloud=20quand=20le=20mod=C3=A8le=20local=20est=20inconnu,?= =?UTF-8?q?=20canal=20model=5Fenv/model=5Farg/env=5Fmap,=20jamais=20de=20t?= =?UTF-8?q?oken=20en=20clair=20(closes=20#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apply_provider : providers::resolve_for (flag > agent > défaut) + apply_env_map_resolved (api_key = @secret, base_url/model du provider résolu) - start/run/restart : --provider et --model dans le CLI, le REPL et la complétion ; specs help + man pages - Tests unitaires : résolution, cascade, canal déclaré, erreur provider inconnu (liste), absence de fuite (env sans valeur sk-), fallback embarqué --- ROADMAP.md | 2 +- man/am-restart.1 | 5 +- man/am-run.1 | 5 +- man/am-start.1 | 5 +- src/agent_config.rs | 29 +++- src/cli.rs | 6 + src/commands/mod.rs | 11 +- src/commands/run_cmd.rs | 246 +++++++++++++++++++++++++++++++++- src/help.rs | 16 ++- src/i18n.rs | 1 + src/repl.rs | 6 + tests/instrumentation_test.rs | 4 +- 12 files changed, 320 insertions(+), 16 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b613032..3071ad1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -501,7 +501,7 @@ S'appuie sur #36 (secrets), #40 (profils), #71 (modèle), #66 (sync) — livrés | [#89](https://git.dracodev.net/Projets/agent-manager/issues/89) | ✅ Secrets partagés par provider (namespace keyring + résolution `@secret` fallback) | S | #88 | | [#90](https://git.dracodev.net/Projets/agent-manager/issues/90) | ✅ AgentDef provider/model + flags `--provider/--model/--no-config` à l'install | M | #88 | | [#91](https://git.dracodev.net/Projets/agent-manager/issues/91) | ✅ Configuration post-install : bloc `config:` par agent (env_map + fichiers) | L | #89, #90 | -| [#92](https://git.dracodev.net/Projets/agent-manager/issues/92) | `am run/start --provider` : override au lancement (providers cloud inclus) | M | #90 | +| [#92](https://git.dracodev.net/Projets/agent-manager/issues/92) | ✅ `am run/start --provider` : override au lancement (providers cloud inclus) | M | #90 | Critère de sortie du jalon : un agent installé est opérationnel sans configuration manuelle (tokens au keyring, provider/modèle par défaut). diff --git a/man/am-restart.1 b/man/am-restart.1 index d92c569..f03b2b6 100644 --- a/man/am-restart.1 +++ b/man/am-restart.1 @@ -4,7 +4,7 @@ .SH NAME restart \- Restart an agent: stop, then start with the same options .SH SYNOPSIS -\fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] +\fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] .SH DESCRIPTION Restart an agent: stop, then start with the same options .SH OPTIONS @@ -30,6 +30,9 @@ Apply an environment profile (env + args, defined in the config) \fB\-\-model\fR \fI\fR Local model to use for this run (validated against the local runtimes) .TP +\fB\-\-provider\fR \fI\fR +Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model +.TP \fB\-\-parallel\fR Start every member of a group simultaneously (issue #57) .TP diff --git a/man/am-run.1 b/man/am-run.1 index 5035d00..eeb5825 100644 --- a/man/am-run.1 +++ b/man/am-run.1 @@ -4,7 +4,7 @@ .SH NAME run \- Run the agent command directly with the given arguments (no process management) .SH SYNOPSIS -\fBrun\fR [\fB\-\-model\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR] +\fBrun\fR [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR] .SH DESCRIPTION Run the agent command directly with the given arguments (no process management) .SH OPTIONS @@ -12,6 +12,9 @@ Run the agent command directly with the given arguments (no process management) \fB\-\-model\fR \fI\fR Local model to use for this run (validated against the local runtimes) .TP +\fB\-\-provider\fR \fI\fR +Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model +.TP \fB\-\-container\fR Run the agent inside a container (issue #58) .TP diff --git a/man/am-start.1 b/man/am-start.1 index 81afb00..c7b8b65 100644 --- a/man/am-start.1 +++ b/man/am-start.1 @@ -4,7 +4,7 @@ .SH NAME start \- Start an agent (foreground by default, or detached with \-\-background) .SH SYNOPSIS -\fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] +\fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] .SH DESCRIPTION Start an agent (foreground by default, or detached with \-\-background) .SH OPTIONS @@ -30,6 +30,9 @@ Apply an environment profile (env + args, defined in the config) \fB\-\-model\fR \fI\fR Local model to use for this run (validated against the local runtimes) .TP +\fB\-\-provider\fR \fI\fR +Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model +.TP \fB\-\-parallel\fR Start every member of a group simultaneously (issue #57) .TP diff --git a/src/agent_config.rs b/src/agent_config.rs index a2307e2..623c686 100644 --- a/src/agent_config.rs +++ b/src/agent_config.rs @@ -63,23 +63,46 @@ pub fn provider_pref(agent: &AgentDef) -> Option<&str> { /// model/base_url/provider -> values of the resolved provider. Explicit /// values already present in agent.env are never overwritten. pub fn apply_env_map(env: &mut BTreeMap, agent: &AgentDef, config: &Config) { + apply_env_map_resolved(env, agent, config, None, None); +} + +/// Same as `apply_env_map` but with the provider and model fixed by the +/// caller (issue #92: `am run/start --provider/--model`): the flags win +/// over the agent declaration and the settings default. +pub fn apply_env_map_resolved( + env: &mut BTreeMap, + agent: &AgentDef, + config: &Config, + provider_override: Option<&str>, + model_override: Option<&str>, +) { let Some(cfg) = &agent.config else { return; }; if cfg.env_map.is_empty() { return; } - let Some(pname) = provider_pref(agent).or_else(|| crate::providers::default_name(config)) else { + let Some(pname) = provider_override + .filter(|p| !p.is_empty()) + .or_else(|| provider_pref(agent)) + .or_else(|| crate::providers::default_name(config)) + else { return; }; let Some(def) = crate::providers::get(config, pname) else { return; }; - let model = agent.model.as_deref().or(def.default_model.as_deref()); + let model = model_override + .filter(|m| !m.is_empty()) + .or(agent.model.as_deref()) + .or(def.default_model.as_deref()); for (slot, var) in &cfg.env_map { let value = match slot.as_str() { "api_key" => "@secret".to_string(), - "model" => model.unwrap_or_default().to_string(), + "model" => { + let Some(m) = model else { continue }; + m.to_string() + } "base_url" => def.base_url.clone(), "provider" => pname.to_string(), _ => continue, // unknown slots are ignored diff --git a/src/cli.rs b/src/cli.rs index 2db31fe..a50f287 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -495,6 +495,9 @@ pub enum Command { /// Local model to use for this run (validated against the local runtimes) #[arg(long, value_name = "MODEL")] model: Option, + /// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model + #[arg(long, value_name = "PROVIDER")] + provider: Option, /// Run the agent inside a container (issue #58) #[arg(long)] container: bool, @@ -566,6 +569,9 @@ pub struct StartArgs { /// Local model to use for this run (validated against the local runtimes) #[arg(long, value_name = "MODEL")] pub model: Option, + /// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model + #[arg(long, value_name = "PROVIDER")] + pub provider: Option, /// Start every member of a group simultaneously (issue #57) #[arg(long, action = ArgAction::SetTrue)] pub parallel: bool, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index aa6a962..33c7333 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -226,8 +226,15 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result { Command::Doctor { fix, watch } => { doctor_cmd::run(app, *fix, *watch) } - Command::Run { agent, model, container, args } => { - run_cmd::run(app, agent, model.as_deref(), *container, args) + Command::Run { agent, model, provider, container, args } => { + run_cmd::run( + app, + agent, + model.as_deref(), + provider.as_deref(), + *container, + args, + ) } Command::Service(svc) => match svc { crate::cli::ServiceCmd::Install { agent, autostart } => { diff --git a/src/commands/run_cmd.rs b/src/commands/run_cmd.rs index 79a834f..5139a90 100644 --- a/src/commands/run_cmd.rs +++ b/src/commands/run_cmd.rs @@ -71,13 +71,30 @@ pub fn start_with_cwd(app: &App, opts: &StartArgs, cwd: Option<&std::path::Path> )?; // --model (issue #71): resolve against the local runtimes, then // transmit through the agent's declared channel (env var or flag). + // --provider (issue #92): resolve from the LLM provider registry. let mut extra_args = extra_args; let mut extra_env = extra_env; - if let Some(m) = opts.model.as_deref() { + if let Some(p) = opts.provider.as_deref() { let agent = require_agent(app, &target)?; - let (a, e) = apply_model(app, agent, m)?; + let (a, e) = apply_provider(app, agent, Some(p), opts.model.as_deref())? + .expect("a provider flag was given: apply_provider always resolves or errors"); extra_args.extend(a); extra_env.extend(e); + } else if let Some(m) = opts.model.as_deref() { + let agent = require_agent(app, &target)?; + match apply_model(app, agent, m) { + Ok((a, e)) => { + extra_args.extend(a); + extra_env.extend(e); + } + Err(local_err) => match apply_provider(app, agent, None, Some(m))? { + Some((a, e)) => { + extra_args.extend(a); + extra_env.extend(e); + } + None => return Err(local_err), + }, + } } if let Some(group) = crate::catalog::Catalog::parse_group_selector(&target) { let members = app.catalog.group_members(group); @@ -320,6 +337,7 @@ pub fn run( app: &App, target: &str, model: Option<&str>, + provider: Option<&str>, container: bool, extra: &[OsString], ) -> Result { @@ -333,10 +351,28 @@ pub fn run( .map(|o| o.to_string_lossy().to_string()) .collect(); let mut extra_env: BTreeMap = BTreeMap::new(); - if let Some(m) = model { - let (a, e) = apply_model(app, agent, m)?; + // Issue #92: --provider resolves from the LLM provider registry (cloud); + // --model alone keeps the local-runtimes-first behavior (issue #71) with + // a cloud fallback when the model is unknown locally. + if let Some(p) = provider { + let (a, e) = apply_provider(app, agent, Some(p), model)? + .expect("a provider flag was given: apply_provider always resolves or errors"); extra_args.extend(a); extra_env.extend(e); + } else if let Some(m) = model { + match apply_model(app, agent, m) { + Ok((a, e)) => { + extra_args.extend(a); + extra_env.extend(e); + } + Err(local_err) => match apply_provider(app, agent, None, Some(m))? { + Some((a, e)) => { + extra_args.extend(a); + extra_env.extend(e); + } + None => return Err(local_err), + }, + } } let exec = resolve_exec(app, agent, &extra_args, &extra_env)?; if app.dry_run() { @@ -474,6 +510,70 @@ pub fn apply_model( Ok((args, env)) } +/// Issue #92: resolve `--provider`/`--model` against the LLM provider +/// registry (cloud) and transmit through the agent's declared channels: +/// `model_env` / `model_arg`, or the `config.env_map` slots (issue #91). +/// The token travels as the `@secret` reference resolved by +/// resolve_env_secrets — never on the command line. Returns None when no +/// provider is configured at all (the caller falls back to the +/// local-runtime path). +fn apply_provider( + app: &App, + agent: &AgentDef, + provider: Option<&str>, + model: Option<&str>, +) -> Result, BTreeMap)>> { + let Some((pname, _def, resolved_model)) = crate::providers::resolve_for( + &app.config, + crate::agent_config::provider_pref(agent), + agent.model.as_deref(), + provider, + model, + ) else { + if provider.is_some() { + let known = crate::providers::names(&app.config); + let hint = if known.is_empty() { + "aucun (am providers add --base-url )".to_string() + } else { + known.join(", ") + }; + bail!(crate::tr_fmt!( + "provider '{}' inconnu — providers enregistrés: {}", + provider.unwrap_or(""), + hint + )); + } + return Ok(None); + }; + let mut args: Vec = Vec::new(); + let mut env: BTreeMap = BTreeMap::new(); + // env_map resolved against THIS provider (api_key stays @secret). + crate::agent_config::apply_env_map_resolved( + &mut env, + agent, + &app.config, + Some(pname), + resolved_model, + ); + // The model through the agent's declared channel (model_env > model_arg); + // otherwise the env_map "model" slot already carried it. + if let Some(m) = resolved_model { + if let Some(var) = &agent.model_env { + env.insert(var.clone(), m.to_string()); + } else if let Some(flag) = &agent.model_arg { + args.push(flag.clone()); + args.push(m.to_string()); + } + } + app.log.info(&crate::tr_fmt!( + "provider {} (modèle {}) au lancement de {}", + pname, + resolved_model.unwrap_or("-"), + agent.name + )); + Ok(Some((args, env))) +} + /// Seconds elapsed since an RFC 3339 timestamp (None when unparsable). fn seconds_since(ts: Option<&str>) -> Option { let t = chrono::DateTime::parse_from_rfc3339(ts?).ok()?; @@ -483,3 +583,141 @@ fn seconds_since(ts: Option<&str>) -> Option { .num_seconds(); Some(secs.max(0) as u64) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{AgentConfig, AgentDef, Config}; + use clap::Parser; + + fn test_app(providers_yaml: &str) -> App { + let guard = tempfile::tempdir().unwrap(); + let dir = guard.path().to_path_buf(); + std::mem::forget(guard); + let cfg = dir.join("config.yaml"); + std::fs::write( + &cfg, + format!( + "version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\n{providers_yaml}agents: []\n" + ), + ) + .unwrap(); + let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]); + crate::app::App::from_cli(cli).unwrap() + } + + fn agent(model_env: bool, model_arg: bool) -> AgentDef { + let mut env_map = BTreeMap::new(); + env_map.insert("api_key".to_string(), "MY_KEY".to_string()); + env_map.insert("model".to_string(), "MY_MODEL".to_string()); + env_map.insert("base_url".to_string(), "MY_URL".to_string()); + AgentDef { + name: "demo".to_string(), + display_name: None, + description: None, + category: None, + website: None, + install: None, + dependencies: vec![], + run: Some("demo".to_string()), + args: vec![], + env: BTreeMap::new(), + version: None, + pin_version: None, + model_env: model_env.then(|| "MY_MODEL".to_string()), + model_arg: model_arg.then(|| "--model".to_string()), + provider: None, + model: None, + config: Some(AgentConfig { + env_map, + files: vec![], + provider_default: None, + }), + tags: vec![], + installable: false, + note: None, + hidden: false, + platforms: vec![], + healthcheck: None, + container: None, + cost_model: None, + } + } + + const OPENAI: &str = " default_provider: openai\n providers:\n openai:\n base_url: https://api.openai.com/v1\n default_model: gpt-5.2\n models: [gpt-5.2]\n"; + + #[test] + fn apply_provider_resolves_and_never_leaks_the_token() { + let app = test_app(OPENAI); + let (args, env) = apply_provider(&app, &agent(false, false), Some("openai"), Some("gpt-5.2")) + .unwrap() + .unwrap(); + // The token slot is the @secret reference — never a value. + assert_eq!(env.get("MY_KEY").unwrap(), "@secret"); + assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2"); + assert_eq!(env.get("MY_URL").unwrap(), "https://api.openai.com/v1"); + assert!(!env.values().any(|v| v.contains("sk-")), "{env:?}"); + assert!(args.is_empty()); + } + + #[test] + fn apply_provider_uses_the_declared_model_channel() { + let app = test_app(OPENAI); + // model_env wins over the env_map slot. + let (args, env) = apply_provider(&app, &agent(true, false), Some("openai"), None) + .unwrap() + .unwrap(); + assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2"); + assert!(args.is_empty()); + // model_arg is pushed on the command line. + let (args, _) = apply_provider(&app, &agent(false, true), Some("openai"), Some("gpt-5.2")) + .unwrap() + .unwrap(); + assert_eq!(args, vec!["--model".to_string(), "gpt-5.2".to_string()]); + } + + #[test] + fn apply_provider_unknown_is_an_error_listing_the_registry() { + let app = test_app(OPENAI); + let err = apply_provider(&app, &agent(false, false), Some("nope"), None).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("nope"), "{msg}"); + assert!(msg.contains("openai"), "registry list missing: {msg}"); + } + + #[test] + fn apply_provider_falls_back_to_the_embedded_default_provider() { + // The embedded default catalog ships providers (anthropic★ since + // issue #90): even an empty user config resolves through the cloud. + let app = test_app(""); + let (_, env) = apply_provider(&app, &agent(false, false), None, Some("x")) + .unwrap() + .expect("the embedded default provider resolves"); + // Model "x" is unknown: the env_map model slot is skipped, but the + // provider slot is wired. + assert_eq!(env.get("MY_URL").unwrap(), "https://api.anthropic.com/v1"); + assert_eq!(env.get("MY_KEY").unwrap(), "@secret"); + } + + #[test] + fn model_flag_falls_back_to_the_cloud_registry() { + let app = test_app(OPENAI); + // apply_model bails (no local runtime in the test env) → the caller + // falls back to apply_provider with the default provider. + let local = apply_model(&app, &agent(false, false), "gpt-5.2").unwrap_err(); + assert!(local.to_string().contains("not found on any local runtime")); + let (_, env) = apply_provider(&app, &agent(false, false), None, Some("gpt-5.2")) + .unwrap() + .unwrap(); + assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2"); + assert_eq!(env.get("MY_URL").unwrap(), "https://api.openai.com/v1"); + } + + #[test] + fn config_parses_the_provider_registry_and_agents() { + let _cfg: Config = serde_yaml::from_str(&format!( + "version: \"1.0\"\nsettings:\n{OPENAI}agents: []\n" + )) + .unwrap(); + } +} diff --git a/src/help.rs b/src/help.rs index 6ad08c3..0b98adb 100644 --- a/src/help.rs +++ b/src/help.rs @@ -156,6 +156,18 @@ const START_FLAGS: &[HelpFlag] = &[ value: "", desc: "Run inside the agent's container profile (issue #58)", }, + HelpFlag { + short: "", + long: "--model", + value: "MODEL", + desc: "Model to use: local runtime first, then the provider registry (issue #71/#92)", + }, + HelpFlag { + short: "", + long: "--provider", + value: "PROVIDER", + desc: "Provider of the registry to use (base_url + keyring token + model) (issue #92)", + }, ]; pub static HELP_SPECS: &[HelpSpec] = &[ @@ -911,7 +923,8 @@ pub static HELP_SPECS: &[HelpSpec] = &[ about: "Run the agent command directly with the given arguments (no process management).", search_terms: &["exec", "pass-through", "container", "docker", "podman"], flags: &[ - HelpFlag { short: "", long: "--model", value: "MODEL", desc: "Local model to use (issue #71)" }, + HelpFlag { short: "", long: "--model", value: "MODEL", desc: "Model to use: local runtime first, then the provider registry (issue #71/#92)" }, + HelpFlag { short: "", long: "--provider", value: "PROVIDER", desc: "Provider of the registry to use (base_url + keyring token + model) (issue #92)" }, HelpFlag { short: "", long: "--container", value: "", desc: "Run inside the agent's container profile (issue #58)" }, ], subcommands: &[], @@ -922,6 +935,7 @@ pub static HELP_SPECS: &[HelpSpec] = &[ io: None, examples: &[ HelpExample { desc: "Run an agent command directly.", code: "run claude-code" }, + HelpExample { desc: "Use a provider of the registry.", code: "run claude-code --provider anthropic --model claude-sonnet-4" }, HelpExample { desc: "Pass --help through to the agent (after --).", code: "run claude-code -- --help" }, ], }, diff --git a/src/i18n.rs b/src/i18n.rs index 36d55c9..bfa7f93 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -185,6 +185,7 @@ pub const CATALOG: &[(&str, &str)] = &[ ("aucun provider configuré — voir: am providers add --base-url ", "no provider configured — see: am providers add --base-url "), ("modèle '{}' absent de la liste du provider '{}' — modèles: {}", "model '{}' is not in provider '{}' model list — models: {}"), ("configuré avec le provider {} (modèle {})", "configured with provider {} (model {})"), + ("provider {} (modèle {}) au lancement de {}", "provider {} (model {}) when launching {}"), ("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"), ("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"), ("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"), diff --git a/src/repl.rs b/src/repl.rs index eddaf4c..2ef2c55 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -254,6 +254,7 @@ impl AmCompleter { "--category", "--args", "--env", "--notify", "--timeout", "--yes", "--verbose", "--quiet", "--json", "--dry-run", "--no-color", "--profile", "--installed", "--tag", "--output", "--random", + "--model", "--provider", "--no-config", ], agents, catalog_agents, @@ -1732,6 +1733,8 @@ fn handle_line( agent: rest.first().cloned(), background: flag("--background") || flag("-b"), profile: opt_value("--profile"), + model: opt_value("--model"), + provider: opt_value("--provider"), ..Default::default() }), "stop" => Command::Stop { @@ -1744,6 +1747,8 @@ fn handle_line( agent: rest.first().cloned(), background: flag("--background") || flag("-b"), profile: opt_value("--profile"), + model: opt_value("--model"), + provider: opt_value("--provider"), ..Default::default() }, force: flag("--force"), @@ -1839,6 +1844,7 @@ fn handle_line( Command::Run { agent, model: opt_value("--model"), + provider: opt_value("--provider"), container: flag("--container"), args: rest[1..].iter().map(|s| s.as_str().into()).collect(), } diff --git a/tests/instrumentation_test.rs b/tests/instrumentation_test.rs index fde0346..1f903d4 100644 --- a/tests/instrumentation_test.rs +++ b/tests/instrumentation_test.rs @@ -45,7 +45,7 @@ agents: #[test] fn run_emits_one_run_event_with_exit_code() { let app = app_with_agent("run", &["run", "echo-agent"], ECHO_AGENT); - let code = run_cmd::run(&app, "echo-agent", None, false, &[OsString::from("hello")]).unwrap(); + let code = run_cmd::run(&app, "echo-agent", None, None, false, &[OsString::from("hello")]).unwrap(); assert_eq!(code, 0); let evs = events(&app); assert_eq!(evs.len(), 1, "exactly one event expected, got {:?}", evs); @@ -58,7 +58,7 @@ fn run_emits_one_run_event_with_exit_code() { #[test] fn dry_run_writes_no_event() { let app = app_with_agent("dry", &["run", "echo-agent", "--dry-run"], ECHO_AGENT); - let code = run_cmd::run(&app, "echo-agent", None, false, &[]).unwrap(); + let code = run_cmd::run(&app, "echo-agent", None, None, false, &[]).unwrap(); assert_eq!(code, 0); assert!(events(&app).is_empty(), "dry-run must not append events"); assert!(events::journal_files(&app.events_dir()).is_empty());