feat: v0.7.0 — am run/start --provider/--model : résolution cloud via le registre (base_url + keyring + modèle), fallback cloud quand le modèle local est inconnu, canal model_env/model_arg/env_map, jamais de token en clair (closes #92)

- 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é
This commit is contained in:
2026-08-19 21:05:39 -04:00
parent 68eb170f57
commit 6d51ce9504
12 changed files with 320 additions and 16 deletions
+26 -3
View File
@@ -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<String, String>, 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<String, String>,
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