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
1 change: 1 addition & 0 deletions src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Start
Swap the model, keep the agent
$ {bin} claude --model z-ai/glm-4.7-flash
$ {bin} claude --yolo --model anyrouter/auto[1m]
$ {bin} claude --model anyrouter/free[1m]
$ {bin} claude --model anyrouter/auto[500k]
$ {bin} claude --model auto --effort high

Expand Down
17 changes: 14 additions & 3 deletions src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ pub fn resolve_launch_api_key(
.map(str::to_string)
}

/// Keep `[1m]` / `[500k]` on virtual `anyrouter/auto` so launch can peel it
/// into `provider.min_context`. Concrete ids still drop Claude's `[1m]` tag.
/// Keep `[1m]` / `[500k]` on virtual presets (`anyrouter/auto`, `free`, `byok`, …)
/// so launch can peel them into `provider.min_context`. Concrete ids still drop
/// Claude's `[1m]` tag.
fn launch_model_id(raw: &str) -> String {
let raw = raw.trim();
if crate::config::parse_context_window_suffix(raw).is_some() && crate::spawn::is_auto_model(raw)
if crate::config::parse_context_window_suffix(raw).is_some()
&& crate::spawn::is_virtual_preset(raw)
{
let stem = crate::config::strip_context_window_suffix(raw);
let suffix = &raw[stem.len()..];
Expand Down Expand Up @@ -344,6 +346,15 @@ agents:
resolve_launch_model(&auto_1m, Some(&cfg), claude_profile, "claude"),
"anyrouter/auto[1m]"
);
let mut free_1m = HashMap::new();
free_1m.insert(
"model".into(),
FlagValue::Value("anyrouter/free[1m]".into()),
);
assert_eq!(
resolve_launch_model(&free_1m, Some(&cfg), claude_profile, "claude"),
"anyrouter/free[1m]"
);
let empty = Profile::default();
assert_eq!(
resolve_launch_model(&flags, None, &empty, "claude"),
Expand Down
62 changes: 58 additions & 4 deletions src/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,10 @@ pub fn build_tool_env(input: BuildToolEnvInput<'_>) -> BTreeMap<String, String>
);
env.insert(
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY".into(),
// Discovery remaps unknown ids (including virtual `anyrouter/auto`)
// onto a catalog SKU such as Laguna. Keep it off for auto so the
// Discovery remaps unknown ids (including virtual `anyrouter/*`)
// onto a catalog SKU such as Laguna. Keep it off for presets so the
// gateway still sees the virtual id + extra-body min_context.
if input.tool.enable_gateway_model_discovery && !is_auto_model(input.model) {
if input.tool.enable_gateway_model_discovery && !is_virtual_preset(input.model) {
"1"
} else {
"0"
Expand All @@ -422,7 +422,7 @@ pub fn build_tool_env(input: BuildToolEnvInput<'_>) -> BTreeMap<String, String>
// automatic model fallback, which rides the fable alias on third-party
// providers — falls back to a different model. Slots set explicitly
// (--haiku/--sonnet/--opus/--fable or the profile config) still win.
let pinned = (!is_auto_model(input.model)).then_some(input.model);
let pinned = (!is_virtual_preset(input.model)).then_some(input.model);
let alias = |slot: &Option<String>, default: &str| -> String {
let explicit = slot.as_deref().map(str::trim).filter(|s| !s.is_empty());
match (pinned, explicit) {
Expand Down Expand Up @@ -502,12 +502,35 @@ pub fn effort_args_for(tool_name: &str, effort: Option<&str>) -> Vec<String> {
vec![]
}

/// First-party virtual presets (`anyrouter/auto`, `anyrouter/free`, …).
/// Keep in lockstep with gateway `ANYROUTER_VIRTUAL_MODELS`.
const VIRTUAL_PRESETS: &[&str] = &[
"anyrouter/auto",
"anyrouter/free",
"anyrouter/byok",
"anyrouter/coding",
"anyrouter/agent",
"anyrouter/hermes",
"anyrouter/cowork",
"anyrouter/latest",
];

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

/// Virtual routing presets. `[1m]` / `[500k]` are min_context floors, not listing ids.
pub fn is_virtual_preset(model: &str) -> bool {
if is_auto_model(model) {
return true;
}
let id = catalog_model_id(model);
let id = crate::config::strip_context_window_suffix(&id);
VIRTUAL_PRESETS.iter().any(|p| *p == id)
}

/// Catalog id for display and config. Auto is `anyrouter/auto`.
pub fn display_model_id(model: &str) -> String {
let id = catalog_model_id(model);
Expand Down Expand Up @@ -1399,6 +1422,18 @@ mod tests {
let body = routing.extra_body_json().expect("body");
assert!(body.contains("\"min_context\":1000000"), "{body}");
assert!(body.contains("\"sort\":\"exacto\""), "{body}");
for id in [
"anyrouter/free[1m]",
"anyrouter/byok[1m]",
"anyrouter/hermes[500k]",
"anyrouter/latest[1m]",
] {
let mut r = crate::config::RoutingConstraints::default();
let catalog = apply_model_id_routing(id, &mut r);
assert!(is_virtual_preset(id), "{id}");
assert!(!catalog.contains('['), "{catalog}");
assert!(r.min_context.is_some(), "{id}");
}
}

#[test]
Expand Down Expand Up @@ -1430,6 +1465,25 @@ mod tests {
Some("0"),
"auto must not be remapped by catalog discovery"
);
let free = build_tool_env(BuildToolEnvInput {
tool_name: "claude",
tool: &tool,
profile: &profile(),
api_key: "sk-ar-v1-secret",
model: "anyrouter/free[1m]",
effort: None,
context_window: None,
model_map: None,
});
assert_eq!(
free.get("ANTHROPIC_MODEL").map(String::as_str),
Some("anyrouter/free")
);
assert_eq!(
free.get("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY")
.map(String::as_str),
Some("0")
);
let concrete = build_tool_env(BuildToolEnvInput {
tool_name: "claude",
tool: &tool,
Expand Down
71 changes: 71 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2346,6 +2346,77 @@ profiles:
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn claude_dry_run_peels_virtual_preset_1m_into_extra_body() {
let dir = std::env::temp_dir().join(format!("anyr-cli-virtual-1m-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.yaml");
std::fs::write(
&path,
"\
active_profile: default
profiles:
default:
api_key: sk-ar-v1-fixture-key-0001
default_model: auto
",
)
.unwrap();
for (flag, expected) in [
("anyrouter/free[1m]", "anyrouter/free"),
("anyrouter/byok[1m]", "anyrouter/byok"),
("anyrouter/hermes[500k]", "anyrouter/hermes"),
] {
let out = anyr()
.args([
"claude",
"--dry-run",
"--yes",
"--config",
path.to_str().unwrap(),
"--model",
flag,
])
.output()
.expect("claude dry-run virtual[1m]");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(
out.status.code().unwrap_or(1),
0,
"{flag}\n{stdout}{stderr}"
);
assert!(
stdout.contains(&format!("ANTHROPIC_MODEL={expected}")),
"{flag} must keep virtual id:\n{stdout}"
);
assert!(
stdout.contains("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=0"),
"{flag}\n{stdout}"
);
assert!(
stdout.contains("CLAUDE_CODE_EXTRA_BODY="),
"{flag}\n{stdout}"
);
if flag.contains("[1m]") {
assert!(
stdout.contains("\"min_context\":1000000"),
"{flag}\n{stdout}"
);
} else {
assert!(
stdout.contains("\"min_context\":500000"),
"{flag}\n{stdout}"
);
}
assert!(
!stdout.to_ascii_lowercase().contains("laguna"),
"{flag}\n{stdout}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn unsigned_hud_dump_offers_launch_claude() {
// WHY: right after install there is no key yet. Enter on the HUD
Expand Down
Loading