Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7f29baea5 | ||
|
|
cf53c98d98 |
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "1.1.7"
|
version = "1.1.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "1.1.7"
|
version = "1.1.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
|
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+17
@@ -138,6 +138,23 @@
|
|||||||
> (`installable: false`, clé API payante seulement), alias `gemini`/`agy`
|
> (`installable: false`, clé API payante seulement), alias `gemini`/`agy`
|
||||||
> redirigés, groupe `mainstream` et gabarit web mis à jour. Import des
|
> redirigés, groupe `mainstream` et gabarit web mis à jour. Import des
|
||||||
> extensions/skills Gemini : `agy plugin import gemini`.
|
> extensions/skills Gemini : `agy plugin import gemini`.
|
||||||
|
>
|
||||||
|
> 🔧 **Maintenance v1.1.8 (2026-09-01)** : providers **QwenCloud** ajoutés au
|
||||||
|
> registre par défaut — `qwen-cloud` (pay-as-you-go, Model Studio/DashScope,
|
||||||
|
> dashscope-intl…/compatible-mode/v1) et `qwen-cloud-token-plan` (abonnement
|
||||||
|
> Credits, token-plan.ap-southeast-1…/compatible-mode/v1, modèles restreints
|
||||||
|
> dont qwen3.8-max). `qwen-code` pointe sur qwen-cloud par défaut (env_map
|
||||||
|
> OpenAI-compatible OPENAI_API_KEY/BASE_URL/MODEL) avec hints pour basculer
|
||||||
|
> sur le Token Plan (`--provider qwen-cloud-token-plan`, clé dédiée via
|
||||||
|
> `am providers set-token`).
|
||||||
|
>
|
||||||
|
> 🐛 **Maintenance v1.1.9 (2026-09-01)** : l'assistant `am setup` listait ses
|
||||||
|
> providers dans une constante codée en dur — les QwenCloud ajoutés au
|
||||||
|
> registre n'y apparaissaient pas. La liste est maintenant construite depuis
|
||||||
|
> le registre fusionné (embarqué + config utilisateur), ollama et custom en
|
||||||
|
> plus, sans doublon. Test de non-régression ajouté. Bonus : le wizard ne
|
||||||
|
> boucle plus à l'infini sur stdin fermé (erreur claire + suggestion
|
||||||
|
> `--yes`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+23
@@ -108,6 +108,20 @@ settings:
|
|||||||
base_url: https://generativelanguage.googleapis.com/v1beta
|
base_url: https://generativelanguage.googleapis.com/v1beta
|
||||||
default_model: gemini-2.5-pro
|
default_model: gemini-2.5-pro
|
||||||
models: [gemini-2.5-pro, gemini-2.5-flash]
|
models: [gemini-2.5-pro, gemini-2.5-flash]
|
||||||
|
# QwenCloud (Alibaba) — deux modes de facturation, deux providers :
|
||||||
|
# - qwen-cloud-token-plan : abonnement Credits (Personal/Team), clé
|
||||||
|
# dédiée Token Plan, modèles restreints (voir docs.qwencloud.com).
|
||||||
|
# Base URL internationale (ap-southeast-1) ; la variante Pékin
|
||||||
|
# token-plan.cn-beijing.maas.aliyuncs.com sert la Chine.
|
||||||
|
# - qwen-cloud : pay-as-you-go (clé sk- Model Studio / DashScope).
|
||||||
|
qwen-cloud-token-plan:
|
||||||
|
base_url: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1
|
||||||
|
default_model: qwen3.8-max
|
||||||
|
models: [qwen3.8-max, qwen3.8-flash, qwen3.7-max, qwen3.7-plus, qwen3.6-flash, deepseek-v4-pro, glm-5.2]
|
||||||
|
qwen-cloud:
|
||||||
|
base_url: https://dashscope-intl.aliyuncs.com/compatible-mode/v1
|
||||||
|
default_model: qwen3.7-plus
|
||||||
|
models: [qwen3.8-max, qwen3.7-max, qwen3.7-plus, qwen3.6-flash, qwen3-coder-plus, qwen3-coder-next, deepseek-v4-pro]
|
||||||
openrouter:
|
openrouter:
|
||||||
base_url: https://openrouter.ai/api/v1
|
base_url: https://openrouter.ai/api/v1
|
||||||
default_model: deepseek/deepseek-chat-v3-0324
|
default_model: deepseek/deepseek-chat-v3-0324
|
||||||
@@ -457,6 +471,15 @@ agents:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- { name: node, min_version: "18.0.0", install_hint: "https://nodejs.org" }
|
- { name: node, min_version: "18.0.0", install_hint: "https://nodejs.org" }
|
||||||
run: qwen-code
|
run: qwen-code
|
||||||
|
provider: qwen-cloud
|
||||||
|
config:
|
||||||
|
env_map:
|
||||||
|
api_key: OPENAI_API_KEY
|
||||||
|
model: OPENAI_MODEL
|
||||||
|
base_url: OPENAI_BASE_URL
|
||||||
|
setup_hints:
|
||||||
|
- "am install qwen-code --provider qwen-cloud-token-plan (abonnement Credits Token Plan au lieu du pay-as-you-go)"
|
||||||
|
- "am providers set-token qwen-cloud-token-plan (clé dédiée Token Plan ; pay-as-you-go : clé sk- Model Studio)"
|
||||||
tags: [alibaba, qwen]
|
tags: [alibaba, qwen]
|
||||||
|
|
||||||
- name: grok-build
|
- name: grok-build
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.TH am 1 "am 1.1.7"
|
.TH am 1 "am 1.1.9"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
am \- agent\-manager (am) — manage local AI coding agents
|
am \- agent\-manager (am) — manage local AI coding agents
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -250,4 +250,4 @@ Export the configuration and installation state (backup)
|
|||||||
am\-import(1)
|
am\-import(1)
|
||||||
Import a previously exported configuration and state
|
Import a previously exported configuration and state
|
||||||
.SH VERSION
|
.SH VERSION
|
||||||
v1.1.7
|
v1.1.9
|
||||||
|
|||||||
+67
-16
@@ -14,9 +14,22 @@ use crate::secrets::SecretStore;
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
/// Known providers offered by the wizard (from the embedded registry plus
|
/// Providers offered by the wizard: every provider defined in the merged
|
||||||
/// the local Ollama runtime and a custom endpoint).
|
/// registry (embedded + user config, sorted), plus the local Ollama runtime
|
||||||
const CHOICES: &[&str] = &["anthropic", "openai", "deepseek", "google", "openrouter", "ollama", "custom"];
|
/// and a custom endpoint. Built dynamically so new registry providers
|
||||||
|
/// (e.g. the QwenCloud token-plan / pay-as-you-go pair) show up without a
|
||||||
|
/// code change — CHOICES used to be a hardcoded list that silently drifted.
|
||||||
|
fn choices(app: &App) -> Vec<String> {
|
||||||
|
let mut v: Vec<String> = crate::providers::defined(&app.config)
|
||||||
|
.map(|(k, _)| k.clone())
|
||||||
|
.collect();
|
||||||
|
v.sort();
|
||||||
|
if !v.iter().any(|p| p == "ollama") {
|
||||||
|
v.push("ollama".to_string());
|
||||||
|
}
|
||||||
|
v.push("custom".to_string());
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
/// True when the user has not configured a usable default provider yet:
|
/// True when the user has not configured a usable default provider yet:
|
||||||
/// no `settings.default_provider`, or the default provider has no API
|
/// no `settings.default_provider`, or the default provider has no API
|
||||||
@@ -36,10 +49,14 @@ pub fn needs_setup(app: &App) -> bool {
|
|||||||
.is_none()
|
.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_line() -> Result<String> {
|
/// Read one trimmed line from stdin. `None` on EOF — callers must not spin
|
||||||
|
/// on empty input when nothing is connected (pipes, --yes in CI).
|
||||||
|
fn read_line() -> Result<Option<String>> {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
std::io::stdin().read_line(&mut line)?;
|
if std::io::stdin().read_line(&mut line)? == 0 {
|
||||||
Ok(line.trim().to_string())
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(line.trim().to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask a y/N question on stderr, defaulting to yes when `yes` is set.
|
/// Ask a y/N question on stderr, defaulting to yes when `yes` is set.
|
||||||
@@ -50,7 +67,7 @@ fn ask(app: &App, question: &str, default_yes: bool) -> Result<bool> {
|
|||||||
let suffix = if default_yes { "[Y/n]" } else { "[y/N]" };
|
let suffix = if default_yes { "[Y/n]" } else { "[y/N]" };
|
||||||
eprint!("{question} {suffix} ");
|
eprint!("{question} {suffix} ");
|
||||||
std::io::stderr().flush()?;
|
std::io::stderr().flush()?;
|
||||||
let answer = read_line()?.to_lowercase();
|
let answer = read_line()?.unwrap_or_default().to_lowercase();
|
||||||
Ok(match answer.as_str() {
|
Ok(match answer.as_str() {
|
||||||
"" => default_yes,
|
"" => default_yes,
|
||||||
"y" | "yes" | "o" | "oui" | "true" => true,
|
"y" | "yes" | "o" | "oui" | "true" => true,
|
||||||
@@ -61,9 +78,10 @@ fn ask(app: &App, question: &str, default_yes: bool) -> Result<bool> {
|
|||||||
/// Pick a provider from the list (or the current default when --yes).
|
/// Pick a provider from the list (or the current default when --yes).
|
||||||
fn pick_provider(app: &App) -> Result<(String, Option<String>)> {
|
fn pick_provider(app: &App) -> Result<(String, Option<String>)> {
|
||||||
let current = crate::providers::default_name(&app.config);
|
let current = crate::providers::default_name(&app.config);
|
||||||
|
let choices = choices(app);
|
||||||
println!("Provider disponible :");
|
println!("Provider disponible :");
|
||||||
for (i, p) in CHOICES.iter().enumerate() {
|
for (i, p) in choices.iter().enumerate() {
|
||||||
let star = if Some(*p) == current { " (actuel)" } else { "" };
|
let star = if Some(p.as_str()) == current { " (actuel)" } else { "" };
|
||||||
println!(" {}) {}{star}", i + 1, p);
|
println!(" {}) {}{star}", i + 1, p);
|
||||||
}
|
}
|
||||||
if app.cli.yes {
|
if app.cli.yes {
|
||||||
@@ -71,16 +89,18 @@ fn pick_provider(app: &App) -> Result<(String, Option<String>)> {
|
|||||||
return Ok((name, None));
|
return Ok((name, None));
|
||||||
}
|
}
|
||||||
loop {
|
loop {
|
||||||
print!("Choisissez (1-{}) : ", CHOICES.len());
|
print!("Choisissez (1-{}) : ", choices.len());
|
||||||
std::io::stdout().flush()?;
|
std::io::stdout().flush()?;
|
||||||
let raw = read_line()?;
|
let Some(raw) = read_line()? else {
|
||||||
|
bail!("aucune entrée sur stdin — relancez en terminal interactif ou utilisez 'am setup --yes'");
|
||||||
|
};
|
||||||
if let Ok(n) = raw.parse::<usize>() {
|
if let Ok(n) = raw.parse::<usize>() {
|
||||||
if (1..=CHOICES.len()).contains(&n) {
|
if (1..=choices.len()).contains(&n) {
|
||||||
let name = CHOICES[n - 1];
|
let name = &choices[n - 1];
|
||||||
if name == "custom" {
|
if name == "custom" {
|
||||||
print!("Base URL de l'API (ex: https://api.openai.com/v1) : ");
|
print!("Base URL de l'API (ex: https://api.openai.com/v1) : ");
|
||||||
std::io::stdout().flush()?;
|
std::io::stdout().flush()?;
|
||||||
let url = read_line()?;
|
let url = read_line()?.unwrap_or_default();
|
||||||
if url.is_empty() {
|
if url.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -126,7 +146,9 @@ fn pick_model(app: &App, name: &str) -> Result<String> {
|
|||||||
loop {
|
loop {
|
||||||
print!("Modèle par défaut : ");
|
print!("Modèle par défaut : ");
|
||||||
std::io::stdout().flush()?;
|
std::io::stdout().flush()?;
|
||||||
let raw = read_line()?;
|
let Some(raw) = read_line()? else {
|
||||||
|
bail!("aucune entrée sur stdin — relancez en terminal interactif ou utilisez 'am setup --yes'");
|
||||||
|
};
|
||||||
if let Ok(n) = raw.parse::<usize>() {
|
if let Ok(n) = raw.parse::<usize>() {
|
||||||
if n >= 1 && n <= models.len() {
|
if n >= 1 && n <= models.len() {
|
||||||
return Ok(models[n - 1].clone());
|
return Ok(models[n - 1].clone());
|
||||||
@@ -134,7 +156,7 @@ fn pick_model(app: &App, name: &str) -> Result<String> {
|
|||||||
if n == models.len() + 1 {
|
if n == models.len() + 1 {
|
||||||
print!("Nom du modèle : ");
|
print!("Nom du modèle : ");
|
||||||
std::io::stdout().flush()?;
|
std::io::stdout().flush()?;
|
||||||
let custom = read_line()?;
|
let custom = read_line()?.unwrap_or_default();
|
||||||
if !custom.is_empty() {
|
if !custom.is_empty() {
|
||||||
return Ok(custom);
|
return Ok(custom);
|
||||||
}
|
}
|
||||||
@@ -347,6 +369,35 @@ mod tests {
|
|||||||
assert!(needs_setup(&app));
|
assert!(needs_setup(&app));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wizard_offers_every_registry_provider() {
|
||||||
|
// Regression (v1.1.8): the wizard used a hardcoded CHOICES list and
|
||||||
|
// the QwenCloud providers added to the registry never appeared.
|
||||||
|
// The list must now track the merged registry + ollama + custom.
|
||||||
|
let app = app_with("");
|
||||||
|
let c = choices(&app);
|
||||||
|
for p in [
|
||||||
|
"anthropic",
|
||||||
|
"openai",
|
||||||
|
"deepseek",
|
||||||
|
"google",
|
||||||
|
"openrouter",
|
||||||
|
"qwen-cloud",
|
||||||
|
"qwen-cloud-token-plan",
|
||||||
|
"ollama",
|
||||||
|
"custom",
|
||||||
|
] {
|
||||||
|
assert!(c.iter().any(|x| x == p), "wizard must offer {p}: {c:?}");
|
||||||
|
}
|
||||||
|
// User-defined providers show up too, ollama is never duplicated.
|
||||||
|
let app2 = app_with(
|
||||||
|
" default_provider: anthropic\n providers:\n ollama:\n base_url: http://localhost:11434/v1\n my-llm:\n base_url: https://llm.internal/v1\n",
|
||||||
|
);
|
||||||
|
let c2 = choices(&app2);
|
||||||
|
assert_eq!(c2.iter().filter(|x| *x == "ollama").count(), 1, "{c2:?}");
|
||||||
|
assert!(c2.iter().any(|x| x == "my-llm"), "{c2:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn needs_setup_false_for_local_ollama() {
|
fn needs_setup_false_for_local_ollama() {
|
||||||
let app = app_with(
|
let app = app_with(
|
||||||
|
|||||||
Reference in New Issue
Block a user