Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2019,8 +2019,10 @@ fn run_launch(
profile.base_url = Some(base);
let aliases_changed = apply_claude_alias_flags(&mut profile, parsed);
let tool = resolve_tool(existing.as_ref(), tool_name)?;
let model = get_string_flag(&parsed.flags, "model")
.unwrap_or_else(|| profile.default_model().to_string());
let model = crate::spawn::sanitize_model_id(
&get_string_flag(&parsed.flags, "model")
.unwrap_or_else(|| profile.default_model().to_string()),
);
let effort = normalize_effort(get_string_flag(&parsed.flags, "effort").as_deref())?;
let model_mode = if is_auto_model(&model) {
"auto"
Expand Down Expand Up @@ -2081,7 +2083,7 @@ fn run_launch(
// Remember the model this launch used as the session default, so a bare
// `{bin} claude` next time starts with it.
if let Some(flag_model) = get_string_flag(&parsed.flags, "model") {
let id = display_model_id(&flag_model).to_string();
let id = display_model_id(&crate::spawn::sanitize_model_id(&flag_model)).to_string();
if let Some(p) = cfg.profiles.get_mut(&cfg.active_profile) {
p.default_model = Some(id);
}
Expand Down
100 changes: 77 additions & 23 deletions src/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ pub fn build_tool_env(input: BuildToolEnvInput<'_>) -> BTreeMap<String, String>
let model_id = pi_resolved_model(input.model);
env.insert(
"PI_MODELS_JSON".into(),
serde_json::to_string(&pi_models_config(&base, model_id))
serde_json::to_string(&pi_models_config(&base, &model_id))
.unwrap_or_else(|_| "{}".into()),
);
}
Expand Down Expand Up @@ -444,7 +444,7 @@ pub fn effort_args_for(tool_name: &str, effort: Option<&str>) -> Vec<String> {
}

pub fn is_auto_model(model: &str) -> bool {
let value = model.trim();
let value = sanitize_model_id(model);
value.is_empty() || value == "auto" || value == "anyrouter/auto"
}

Expand All @@ -456,11 +456,45 @@ pub fn display_model_id(model: &str) -> &str {
}
}

pub fn pi_resolved_model(model: &str) -> &str {
if model.is_empty() || model == "auto" {
PI_DEFAULT_MODEL
/// Strip CSI color/bold sequences (and dangling `[1m` tails if ESC was already
/// dropped). A TUI-copied id like `stealth/ox-alpha[1m` 404s at the gateway.
pub fn sanitize_model_id(model: &str) -> String {
let mut s = String::with_capacity(model.len());
let mut chars = model.trim().chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
if chars.peek() == Some(&'[') {
chars.next();
for n in chars.by_ref() {
if n.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
s.push(c);
}
if let Some(i) = s.rfind('[') {
let tail = &s[i + 1..];
if tail.ends_with('m')
&& tail.len() > 1
&& tail[..tail.len() - 1]
.bytes()
.all(|b| b.is_ascii_digit() || b == b';')
{
s.truncate(i);
}
}
s.trim().to_string()
}

pub fn pi_resolved_model(model: &str) -> String {
let s = sanitize_model_id(model);
if is_auto_model(&s) {
PI_DEFAULT_MODEL.to_string()
} else {
model
s
}
}

Expand Down Expand Up @@ -498,8 +532,8 @@ pub fn prepare_pi_wrapper(
let dir = pi_agent_dir(config_path);
let model_id = pi_resolved_model(model);
let base = tool_base_url(profile, tool);
let models = pi_models_config(&base, model_id);
write_pi_wrapper_files(&dir, &models, model_id)?;
let models = pi_models_config(&base, &model_id);
write_pi_wrapper_files(&dir, &models, &model_id)?;
env.insert(
"PI_CODING_AGENT_DIR".into(),
dir.to_string_lossy().into_owned(),
Expand Down Expand Up @@ -553,9 +587,10 @@ fn write_pi_wrapper_files(
pub fn model_args_for(tool_name: &str, model: &str, model_mode: &str) -> Vec<String> {
if tool_name == "pi" {
let id = pi_resolved_model(model);
// Pi strips a matching `--provider` prefix from `--model`. Prefix so
// ids like `anyrouter/free` survive as the AnyRouter model id.
return vec!["--model".into(), format!("anyrouter/{id}")];
// Official AnyRouter Pi guide: `pi --provider anyrouter --model "<catalog-id>"`.
// The provider is already `--provider anyrouter`; do not prefix it again
// (`anyrouter/stealth/ox-alpha` 404s — the catalog id is `stealth/ox-alpha`).
return vec!["--model".into(), id];
}
if model.is_empty() || model == "auto" || model_mode == "auto" {
return vec![];
Expand Down Expand Up @@ -677,6 +712,22 @@ mod tests {
default_profile_for_env(None, Some("sk-ar-v1-secret"))
}

#[test]
fn sanitize_model_id_strips_ansi_and_dangling_sgr() {
assert_eq!(sanitize_model_id("stealth/ox-alpha"), "stealth/ox-alpha");
assert_eq!(
sanitize_model_id("stealth/ox-alpha\u{1b}[1m"),
"stealth/ox-alpha"
);
assert_eq!(sanitize_model_id("stealth/ox-alpha[1m"), "stealth/ox-alpha");
assert_eq!(
sanitize_model_id("\u{1b}[1mstealth/ox-alpha\u{1b}[0m"),
"stealth/ox-alpha"
);
assert_eq!(pi_resolved_model("anyrouter/auto"), PI_DEFAULT_MODEL);
assert_eq!(pi_resolved_model("auto"), PI_DEFAULT_MODEL);
}

#[test]
fn redact_auth_token_and_keep_numeric() {
assert_eq!(
Expand Down Expand Up @@ -908,24 +959,27 @@ mod tests {
);
assert_eq!(
model_args_for("pi", "z-ai/glm-4.7-flash", "concrete"),
vec![
"--model".to_string(),
"anyrouter/z-ai/glm-4.7-flash".to_string()
]
vec!["--model".to_string(), "z-ai/glm-4.7-flash".to_string()]
);
assert_eq!(
model_args_for("pi", "anyrouter/free", "concrete"),
vec![
"--model".to_string(),
"anyrouter/anyrouter/free".to_string()
]
vec!["--model".to_string(), "anyrouter/free".to_string()]
);
assert_eq!(
model_args_for("pi", "stealth/ox-alpha", "concrete"),
vec!["--model".to_string(), "stealth/ox-alpha".to_string()]
);
assert_eq!(
model_args_for("pi", "stealth/ox-alpha[1m", "concrete"),
vec!["--model".to_string(), "stealth/ox-alpha".to_string()]
);
assert_eq!(
model_args_for("pi", "auto", "auto"),
vec![
"--model".to_string(),
format!("anyrouter/{PI_DEFAULT_MODEL}")
]
vec!["--model".to_string(), PI_DEFAULT_MODEL.to_string()]
);
assert_eq!(
model_args_for("pi", "anyrouter/auto", "auto"),
vec!["--model".to_string(), PI_DEFAULT_MODEL.to_string()]
);
}

Expand Down
6 changes: 5 additions & 1 deletion tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,11 @@ fn pi_dry_run_uses_anyrouter_provider() {
assert!(stdout.contains("--provider"), "{stdout}");
assert!(stdout.contains("anyrouter"), "{stdout}");
assert!(stdout.contains("--model"), "{stdout}");
assert!(stdout.contains("anyrouter/z-ai/glm-4.7-flash"), "{stdout}");
assert!(stdout.contains("z-ai/glm-4.7-flash"), "{stdout}");
assert!(
!stdout.contains("anyrouter/z-ai/glm-4.7-flash"),
"Pi --model is the catalog id, not anyrouter/<id>:\n{stdout}"
);
assert!(stdout.contains("PI_CODING_AGENT_DIR"), "{stdout}");
assert!(stdout.contains("ANYROUTER_API_KEY"), "{stdout}");
assert!(stdout.contains("anyrouter.dev/api/v1"), "{stdout}");
Expand Down
Loading