From 597775925b40d727b5b636b413cbc8f0b5edc8fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 13:00:12 +0000 Subject: [PATCH 1/2] feat(tui): per-agent routing filters (exacto, tools, 1M ctx) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONFIGURE exposes Exacto / require-tools / ≥1M-context toggles for the highlighted coding agent. Values persist under agents. using AnyRouter preset field names (provider.sort, require_params, min_context) and launch forwards them via CLAUDE_CODE_EXTRA_BODY. The model picker stays anyrouter/auto; LAUNCH stays above CONFIGURE. Closes #43 Co-authored-by: duyet --- .cursor/skills/verify-anyr/features/README.md | 1 + .../verify-anyr/features/routing-filters.md | 37 +++ src/commands.rs | 259 +++++++++++++++++- src/config.rs | 203 +++++++++++++- src/help.rs | 5 +- src/spawn.rs | 49 +++- src/tui/state.rs | 129 ++++++++- src/tui/view.rs | 5 + tests/cli.rs | 136 ++++++++- 9 files changed, 796 insertions(+), 28 deletions(-) create mode 100644 .cursor/skills/verify-anyr/features/routing-filters.md diff --git a/.cursor/skills/verify-anyr/features/README.md b/.cursor/skills/verify-anyr/features/README.md index f698692..1dfd6d9 100644 --- a/.cursor/skills/verify-anyr/features/README.md +++ b/.cursor/skills/verify-anyr/features/README.md @@ -54,4 +54,5 @@ Keep implementation details out of the map. Name only user paths, stable handles - [Model picker catalog](./model-picker.md) covers the inline model picker pinning `anyrouter/auto` without a most-used dump. - [Accounts and config](./accounts-and-config.md) covers whoami, account switch, config path, and logout against the isolated home. - [Per-agent bindings](./per-agent-bindings.md) covers inline model/account/key binds per coding agent and launch that honors per-agent keys. +- [Routing filters](./routing-filters.md) covers exacto / tools / 1M ctx toggles per agent and launch extra-body fields. - [Update progress](./update-progress.md) covers `anyr update` from→to + channel copy, the success hint, and a TTY spinner that actually ticks. diff --git a/.cursor/skills/verify-anyr/features/routing-filters.md b/.cursor/skills/verify-anyr/features/routing-filters.md new file mode 100644 index 0000000..25a689d --- /dev/null +++ b/.cursor/skills/verify-anyr/features/routing-filters.md @@ -0,0 +1,37 @@ +# Routing filters (exacto, tools, 1M ctx) + +CONFIGURE exposes Exacto / require-tools / ≥1M-context toggles for the current coding agent. Values persist per agent in `config.yaml` using AnyRouter preset field names (`provider.sort`, `require_params`, `min_context`) and launch forwards them on the request. The default model picker stays `anyrouter/auto`. + +## Sub-features + +- `toggles-persist` writes exacto / tools / 1M ctx per agent in `agents.` without changing sibling agents. +- `launch-sends` dry-run of `anyr claude --model anyrouter/auto` (and `anyrouter/free`) includes `CLAUDE_CODE_EXTRA_BODY` with those fields. +- `picker-auto` still pins `anyrouter/auto` and does not dump most-used models. +- `launch-above-configure` keeps `LAUNCH` above `CONFIGURE` in the home dump. + +## How to get to it (user POV) + +- TUI: `anyr` / `anyr menu`. Highlight an agent, then toggle `exacto` / `tools` / `1M ctx` on the same CONFIGURE list. +- Settings: `anyr config`, agent tab, Routing rows. Enter toggles; `x` turns a row off. +- Launch: `anyr claude --model anyrouter/auto --dry-run --yes` (always `--dry-run` here). Same for `anyrouter/free`. +- `--yes` / `--ok` keep the stored routing settings. + +## Driving it with control-anyr + +Preconditions: + +- `control-anyr doctor` is clean. +- Isolated config has profiles `default` and `work`. +- Helper sets `ANYR_NO_CATALOG=1`. Do not invent catalog ids. Do not use ox-alpha. + +- **Menu dump.** Run `ANYR_AGENTS=claude,grok control-anyr cli --out artifacts/routing-filters/menu-dump.txt -- menu --dump-tui`. Exit `0`. Stdout contains `LAUNCH`, `CONFIGURE`, `exacto`, `tools`, `1M ctx`, `anyrouter/auto` or `for claude`. `LAUNCH` appears before `CONFIGURE`. Stdout does not contain `most used`. +- **Models dump.** Run `control-anyr cli --out artifacts/routing-filters/models-dump.txt -- models --dump-tui`. Exit `0`. Stdout contains `anyrouter/auto`. Stdout does not contain `most used`. +- **Launch sends constraints.** Write routing onto `agents.claude` (`provider.sort: exacto`, `require_params: tools`, `min_context: 1000000`). Run `control-anyr cli --out artifacts/routing-filters/claude-auto.txt -- claude --dry-run --yes --model anyrouter/auto` and `… --model anyrouter/free`. Exit `0`. Stdout contains `CLAUDE_CODE_EXTRA_BODY=`, `"sort":"exacto"`, `"require_params":["tools"]`, `"min_context":1000000`. No agent child. Fixture key redacted. +- **Proof.** Keep the dry-run files, `menu-dump.txt`, and a copy of `config.yaml` after the toggles. + +## Gotchas + +- `--yes` / `--ok` without `--dry-run` starts Claude. Skip that. +- Do not invent model ids. Real presets are `anyrouter/auto` and `anyrouter/free`. +- Routing is per agent (#35/#36). Toggling claude must not rewrite grok. +- Logo restore / “don’t flash CONFIGURE over LAUNCH” stays a home-TUI invariant (#36). diff --git a/src/commands.rs b/src/commands.rs index f25f771..636d5d1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -22,10 +22,10 @@ use crate::key::{ }; use crate::parse::{get_string_flag, parse_cli_args, FlagValue, ParsedArgs}; use crate::spawn::{ - build_tool_env, canonical_tool, catalog_model_id, default_profile_for_env, display_model_id, - effort_args_for, env_command_path, is_auto_model, model_args_for, normalize_effort, - prepare_pi_wrapper, provider_args_for, render_dry_run, resolve_tool, session_model_label, - spawn_child, BuildToolEnvInput, + apply_routing_env, build_tool_env, canonical_tool, catalog_model_id, default_profile_for_env, + display_model_id, effort_args_for, env_command_path, is_auto_model, model_args_for, + normalize_effort, prepare_pi_wrapper, provider_args_for, render_dry_run, resolve_tool, + session_model_label, spawn_child, BuildToolEnvInput, }; use crate::term; use crate::VERSION; @@ -1041,6 +1041,43 @@ fn palette_bind_detail(agent: &str) -> String { format!("for {agent}") } +fn routing_toggle_detail(on: bool, agent: &str) -> String { + format!("{} · for {agent}", if on { "on" } else { "off" }) +} + +fn toggle_agent_routing_field( + path: &std::path::Path, + agent: &str, + field: RoutingField, +) -> Result { + let agent = known_agent(agent)?; + let mut cfg = load_config_if_present(path).unwrap_or_default(); + let routing = &mut cfg.agent_binding_mut(&agent).routing; + let on = match field { + RoutingField::Exacto => { + routing.set_exacto(!routing.wants_exacto()); + routing.wants_exacto() + } + RoutingField::Tools => { + routing.set_require_tools(!routing.requires_tools()); + routing.requires_tools() + } + RoutingField::MinContext => { + routing.set_require_1m(!routing.requires_1m_context()); + routing.requires_1m_context() + } + }; + cfg.prune_empty_agents(); + write_config(&cfg, path)?; + println!( + "{} {agent} {} {}", + term::ok("Saved"), + field.label(), + if on { "on" } else { "off" } + ); + Ok(0) +} + fn run_models(parsed: &ParsedArgs, env: &BTreeMap) -> Result { let path = config_path(parsed, env); let existing = load_config_if_present(&path); @@ -1369,6 +1406,33 @@ enum SettingKind { AgentAccount(&'static str), AgentKey(&'static str), AgentModel(&'static str), + AgentRouting(&'static str, RoutingField), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RoutingField { + Exacto, + Tools, + MinContext, +} + +impl RoutingField { + fn label(self) -> &'static str { + match self { + RoutingField::Exacto => "exacto", + RoutingField::Tools => "tools", + RoutingField::MinContext => "1M ctx", + } + } + + #[allow(dead_code)] + fn action_kind(self) -> &'static str { + match self { + RoutingField::Exacto => "exacto", + RoutingField::Tools => "tools", + RoutingField::MinContext => "1m", + } + } } fn run_config_tui(parsed: &ParsedArgs, env: &BTreeMap) -> Result { @@ -1752,6 +1816,57 @@ fn fill_agent_settings( SettingKind::AgentModel(id), ); + section(rows, kinds, "Routing"); + let routing = binding.map(|b| b.routing.clone()).unwrap_or_default(); + entry( + rows, + kinds, + "exacto", + if routing.wants_exacto() { + "on".into() + } else { + "off".into() + }, + if routing.wants_exacto() { + Tone::Good + } else { + Tone::Muted + }, + SettingKind::AgentRouting(id, RoutingField::Exacto), + ); + entry( + rows, + kinds, + "tools", + if routing.requires_tools() { + "on".into() + } else { + "off".into() + }, + if routing.requires_tools() { + Tone::Good + } else { + Tone::Muted + }, + SettingKind::AgentRouting(id, RoutingField::Tools), + ); + entry( + rows, + kinds, + "1M ctx", + if routing.requires_1m_context() { + "on".into() + } else { + "off".into() + }, + if routing.requires_1m_context() { + Tone::Good + } else { + Tone::Muted + }, + SettingKind::AgentRouting(id, RoutingField::MinContext), + ); + section(rows, kinds, "Mapping"); entry( rows, @@ -2050,6 +2165,7 @@ fn config_edit_row( other => other, } } + SettingKind::AgentRouting(id, field) => toggle_agent_routing_field(path, id, field), SettingKind::Channel => { let choices = ["stable", "beta"]; let current = choices.iter().position(|c| *c == cfg_channel(path)); @@ -2206,6 +2322,45 @@ fn config_reset_row(path: &std::path::Path, kind: SettingKind) -> Result { + let was_set = { + let Some(b) = cfg.agents.get_mut(id) else { + println!( + "{}", + term::dim(&format!("{id} {} already off", field.label())) + ); + return Ok(0); + }; + match field { + RoutingField::Exacto => { + let on = b.routing.wants_exacto(); + b.routing.set_exacto(false); + on + } + RoutingField::Tools => { + let on = b.routing.requires_tools(); + b.routing.set_require_tools(false); + on + } + RoutingField::MinContext => { + let on = b.routing.requires_1m_context(); + b.routing.set_require_1m(false); + on + } + } + }; + cfg.prune_empty_agents(); + if !was_set { + println!( + "{}", + term::dim(&format!("{id} {} already off", field.label())) + ); + return Ok(0); + } + write_config(&cfg, path)?; + println!("{} {id} {} off", term::ok("Saved"), field.label()); + Ok(0) + } SettingKind::GatewayDiscovery => { let mut cfg = load_config_if_present(path).unwrap_or_default(); if let Some(t) = cfg.tools.get_mut("claude") { @@ -2479,6 +2634,12 @@ fn run_launch( context_window: resolved.context_window, model_map: None, }); + let routing = existing + .as_ref() + .and_then(|c| c.agent_binding(tool_name)) + .map(|b| b.routing.clone()) + .unwrap_or_default(); + apply_routing_env(&mut env_map, &routing, tool_name); if tool_name == "pi" { prepare_pi_wrapper(&mut env_map, &path, &profile, &tool, &model)?; } @@ -2994,7 +3155,9 @@ fn agent_binding_detail(cfg: &crate::config::Config, id: &str, signed_in: bool) .map(str::trim) .filter(|s| !s.is_empty()) .map(session_model_label) - .unwrap_or_else(|| session_model_label(profile.map(|p| p.default_model()).unwrap_or("auto"))); + .unwrap_or_else(|| { + session_model_label(profile.map(|p| p.default_model()).unwrap_or("auto")) + }); let account = binding .and_then(|b| b.profile.as_deref()) .map(str::trim) @@ -3013,7 +3176,19 @@ fn agent_binding_detail(cfg: &crate::config::Config, id: &str, signed_in: bool) } else { "(not signed in)".into() }; - format!("{model} · {account} · {key}") + let mut parts = vec![model, account.to_string(), key]; + if let Some(r) = binding.map(|b| &b.routing) { + if r.wants_exacto() { + parts.push("exacto".into()); + } + if r.requires_tools() { + parts.push("tools".into()); + } + if r.requires_1m_context() { + parts.push("1M".into()); + } + } + parts.join(" · ") } /// Palette entries: launch rows first (bindings visible per agent), then @@ -3078,7 +3253,7 @@ fn launcher_palette( .map(|(id, _)| (*id).to_string()) .unwrap_or(last.clone()) }; - push_agent_configure_entries(&mut entries, &hub); + push_agent_configure_entries(&mut entries, &hub, &cfg); entries.push(PaletteEntry::new( "install…", "install a coding agent", @@ -3146,7 +3321,7 @@ fn launcher_palette( format!("Launch {id}"), )); } - push_inline_configure(&mut entries, &last); + push_inline_configure(&mut entries, &last, &cfg); } } else { entries.push(InlineEntry::new( @@ -3155,7 +3330,7 @@ fn launcher_palette( "account", "Login / sign in", )); - push_inline_configure(&mut entries, &last); + push_inline_configure(&mut entries, &last, &cfg); } entries.push(InlineEntry::new( "install…", @@ -3174,7 +3349,11 @@ fn launcher_palette( } #[cfg(not(feature = "native"))] -fn push_inline_configure(entries: &mut Vec, agent: &str) { +fn push_inline_configure(entries: &mut Vec, agent: &str, cfg: &crate::config::Config) { + let routing = cfg + .agent_binding(agent) + .map(|b| b.routing.clone()) + .unwrap_or_default(); entries.push(InlineEntry::new( "model…", palette_bind_detail(agent), @@ -3193,6 +3372,24 @@ fn push_inline_configure(entries: &mut Vec, agent: &str) { format!("configure · {agent}"), format!("Switch key {agent}"), )); + entries.push(InlineEntry::new( + "exacto", + routing_toggle_detail(routing.wants_exacto(), agent), + format!("configure · {agent}"), + format!("Toggle exacto {agent}"), + )); + entries.push(InlineEntry::new( + "tools", + routing_toggle_detail(routing.requires_tools(), agent), + format!("configure · {agent}"), + format!("Toggle tools {agent}"), + )); + entries.push(InlineEntry::new( + "1M ctx", + routing_toggle_detail(routing.requires_1m_context(), agent), + format!("configure · {agent}"), + format!("Toggle 1m {agent}"), + )); } fn run_menu(parsed: &ParsedArgs, env: &BTreeMap) -> Result { @@ -3315,9 +3512,17 @@ fn push_launch_entries( } #[cfg(feature = "native")] -fn push_agent_configure_entries(entries: &mut Vec, agent: &str) { +fn push_agent_configure_entries( + entries: &mut Vec, + agent: &str, + cfg: &crate::config::Config, +) { use crate::tui::PaletteEntry; let group = format!("configure · {agent}"); + let routing = cfg + .agent_binding(agent) + .map(|b| b.routing.clone()) + .unwrap_or_default(); entries.push(PaletteEntry::new( "model…", palette_bind_detail(agent), @@ -3333,9 +3538,27 @@ fn push_agent_configure_entries(entries: &mut Vec, age entries.push(PaletteEntry::new( "key…", palette_bind_detail(agent), - group, + group.clone(), format!("Switch key {agent}"), )); + entries.push(PaletteEntry::new( + "exacto", + routing_toggle_detail(routing.wants_exacto(), agent), + group.clone(), + format!("Toggle exacto {agent}"), + )); + entries.push(PaletteEntry::new( + "tools", + routing_toggle_detail(routing.requires_tools(), agent), + group.clone(), + format!("Toggle tools {agent}"), + )); + entries.push(PaletteEntry::new( + "1M ctx", + routing_toggle_detail(routing.requires_1m_context(), agent), + group, + format!("Toggle 1m {agent}"), + )); } fn install_agent_dialog(path: &PathBuf, env: &BTreeMap) -> Result { @@ -3609,6 +3832,18 @@ fn launcher_dispatch( if let Some(agent) = action.strip_prefix("Switch key ") { return switch_agent_key(parsed, env, path, agent.trim()); } + if let Some(agent) = action.strip_prefix("Toggle exacto ") { + toggle_agent_routing_field(path, agent.trim(), RoutingField::Exacto)?; + return Ok(LauncherNext::Continue); + } + if let Some(agent) = action.strip_prefix("Toggle tools ") { + toggle_agent_routing_field(path, agent.trim(), RoutingField::Tools)?; + return Ok(LauncherNext::Continue); + } + if let Some(agent) = action.strip_prefix("Toggle 1m ") { + toggle_agent_routing_field(path, agent.trim(), RoutingField::MinContext)?; + return Ok(LauncherNext::Continue); + } if action == "Credits" { if let Err(err) = run_usage(parsed, env) { eprintln!("{}", term::err(&err)); diff --git a/src/config.rs b/src/config.rs index 44c7e2e..82f53ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -114,6 +114,9 @@ pub struct AgentBinding { pub profile: Option, pub api_key: Option, pub default_model: Option, + /// Request-level routing controls. Field names match AnyRouter presets + /// (`provider.sort`, `require_params`, `min_context`). + pub routing: RoutingConstraints, } impl AgentBinding { @@ -135,6 +138,100 @@ impl AgentBinding { .map(str::trim) .unwrap_or("") .is_empty() + && self.routing.is_empty() + } +} + +/// Routing filters forwarded on launch. Same names as AnyRouter preset / +/// chat-completion request fields (API issue sibling). +#[derive(Debug, Clone, PartialEq, Default)] +pub struct RoutingConstraints { + /// `provider.sort` — `"exacto"` when the user wants Exacto quality routing. + pub sort: Option, + /// `require_params` — e.g. `["tools"]` to require tool-calling backends. + pub require_params: Vec, + /// `min_context` — e.g. `1_000_000` to require a ≥1M context window. + pub min_context: Option, +} + +pub const ROUTING_SORT_EXACTO: &str = "exacto"; +pub const ROUTING_PARAM_TOOLS: &str = "tools"; +pub const ROUTING_MIN_1M_CONTEXT: i64 = 1_000_000; + +impl RoutingConstraints { + pub fn is_empty(&self) -> bool { + self.sort.as_deref().map(str::trim).unwrap_or("").is_empty() + && self.require_params.is_empty() + && self.min_context.is_none() + } + + pub fn wants_exacto(&self) -> bool { + self.sort + .as_deref() + .map(str::trim) + .is_some_and(|s| s.eq_ignore_ascii_case(ROUTING_SORT_EXACTO)) + } + + pub fn requires_tools(&self) -> bool { + self.require_params + .iter() + .any(|p| p.trim().eq_ignore_ascii_case(ROUTING_PARAM_TOOLS)) + } + + pub fn requires_1m_context(&self) -> bool { + self.min_context + .is_some_and(|n| n >= ROUTING_MIN_1M_CONTEXT) + } + + pub fn set_exacto(&mut self, on: bool) { + self.sort = on.then(|| ROUTING_SORT_EXACTO.to_string()); + } + + pub fn set_require_tools(&mut self, on: bool) { + self.require_params + .retain(|p| !p.trim().eq_ignore_ascii_case(ROUTING_PARAM_TOOLS)); + if on { + self.require_params.push(ROUTING_PARAM_TOOLS.to_string()); + } + } + + pub fn set_require_1m(&mut self, on: bool) { + self.min_context = on.then_some(ROUTING_MIN_1M_CONTEXT); + } + + /// JSON object merged into the inference request body (Claude + /// `CLAUDE_CODE_EXTRA_BODY`, printed on dry-run for every agent). + pub fn extra_body_json(&self) -> Option { + if self.is_empty() { + return None; + } + let mut body = serde_json::Map::new(); + if let Some(sort) = self + .sort + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + body.insert("provider".into(), serde_json::json!({ "sort": sort })); + } + if !self.require_params.is_empty() { + let params: Vec<&str> = self + .require_params + .iter() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); + if !params.is_empty() { + body.insert("require_params".into(), serde_json::json!(params)); + } + } + if let Some(n) = self.min_context { + body.insert("min_context".into(), serde_json::json!(n)); + } + if body.is_empty() { + return None; + } + Some(serde_json::Value::Object(body).to_string()) } } @@ -442,12 +539,56 @@ fn agent_binding_from_map(map: &BTreeMap) -> AgentBinding { "default_model" => { b.default_model = Some(v.as_string_lossy()).filter(|s| !s.is_empty()) } + "provider" => { + if let Some(pm) = v.as_map() { + if let Some(sort) = pm + .get("sort") + .map(|s| s.as_string_lossy()) + .filter(|s| !s.trim().is_empty()) + { + b.routing.sort = Some(sort); + } + } + } + "sort" => { + let s = v.as_string_lossy(); + if !s.trim().is_empty() { + b.routing.sort = Some(s); + } + } + "require_params" => b.routing.require_params = parse_require_params(v), + "min_context" => { + b.routing.min_context = match v { + YamlValue::Int(n) => Some(*n), + YamlValue::Bool(true) => Some(ROUTING_MIN_1M_CONTEXT), + _ => v.as_string_lossy().parse().ok(), + } + } _ => {} } } b } +fn parse_require_params(value: &YamlValue) -> Vec { + match value { + YamlValue::Bool(true) => vec![ROUTING_PARAM_TOOLS.to_string()], + YamlValue::Bool(false) | YamlValue::Null => Vec::new(), + YamlValue::Map(map) => map + .values() + .map(|v| v.as_string_lossy()) + .filter(|s| !s.trim().is_empty()) + .collect(), + YamlValue::Int(_) | YamlValue::String(_) => value + .as_string_lossy() + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_string) + .collect(), + } +} + pub fn parse_config(source: &str) -> Config { let root = parse_yaml_map(source); let mut config = Config::default(); @@ -591,6 +732,25 @@ pub fn serialize_config(config: &Config) -> String { if let Some(m) = &binding.default_model { lines.push(format!(" default_model: {}", yaml_scalar(m))); } + if let Some(sort) = binding + .routing + .sort + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + lines.push(" provider:".into()); + lines.push(format!(" sort: {}", yaml_scalar(sort))); + } + if !binding.routing.require_params.is_empty() { + lines.push(format!( + " require_params: {}", + yaml_scalar(&binding.routing.require_params.join(",")) + )); + } + if let Some(n) = binding.routing.min_context { + lines.push(format!(" min_context: {n}")); + } } } lines.push("tools:".into()); @@ -787,6 +947,46 @@ agents: ); } + #[test] + fn agents_section_roundtrips_routing_constraints() { + let src = "\ +active_profile: default +profiles: + default: + api_key: sk-ar-v1-test + default_model: auto +agents: + claude: + default_model: anyrouter/auto + provider: + sort: exacto + require_params: tools + min_context: 1000000 + grok: + default_model: anyrouter/free +"; + let cfg = parse_config(src); + let claude = cfg.agent_binding("claude").expect("claude binding"); + assert!(claude.routing.wants_exacto()); + assert!(claude.routing.requires_tools()); + assert!(claude.routing.requires_1m_context()); + let body = claude.routing.extra_body_json().expect("body"); + assert!(body.contains("\"sort\":\"exacto\""), "{body}"); + assert!(body.contains("\"require_params\":[\"tools\"]"), "{body}"); + assert!(body.contains("\"min_context\":1000000"), "{body}"); + let grok = cfg.agent_binding("grok").expect("grok binding"); + assert!(grok.routing.is_empty()); + let yaml = serialize_config(&cfg); + assert!(yaml.contains(" sort: exacto"), "{yaml}"); + assert!(yaml.contains(" require_params: tools"), "{yaml}"); + assert!(yaml.contains(" min_context: 1000000"), "{yaml}"); + let again = parse_config(&yaml); + let claude2 = again.agent_binding("claude").expect("claude"); + assert!(claude2.routing.wants_exacto()); + assert!(claude2.routing.requires_tools()); + assert_eq!(claude2.routing.min_context, Some(1_000_000)); + } + #[test] fn set_active_profile_rejects_unknown() { let cfg = parse_config("active_profile: default\nprofiles:\n default:\n api_key: x\n"); @@ -902,8 +1102,7 @@ profiles: ); cfg.agent_binding_mut("claude").default_model = Some("stealth/ox-alpha".into()); cfg.agent_binding_mut("grok").default_model = Some("grok-4".into()); - cfg.agent_binding_mut("claude").default_model = - Some("anthropic/claude-sonnet-4.6".into()); + cfg.agent_binding_mut("claude").default_model = Some("anthropic/claude-sonnet-4.6".into()); assert_eq!( cfg.agent_binding("claude") .and_then(|b| b.default_model.as_deref()), diff --git a/src/help.rs b/src/help.rs index b084866..cc64784 100644 --- a/src/help.rs +++ b/src/help.rs @@ -365,8 +365,9 @@ USAGE On a TTY, `{bin} config` opens a grouped settings screen — Account, Model, Agent, General — each row showing its current value. ↑↓ / j k navigate, ↵ edits the focused row (switch / add / re-auth / log out accounts, pick a -key or model slot, choose the coding agent, toggle auto-update, switch -channel), x resets a row to its default, q / esc closes. +key or model slot, choose the coding agent, toggle exacto / tools / 1M ctx +routing, toggle auto-update, switch channel), x resets a row to its default, +q / esc closes. Also reachable from the launcher via Config. `--dump-tui` prints one plain frame and exits. "; diff --git a/src/spawn.rs b/src/spawn.rs index 664e328..8ba4f3d 100644 --- a/src/spawn.rs +++ b/src/spawn.rs @@ -445,6 +445,22 @@ pub fn build_tool_env(input: BuildToolEnvInput<'_>) -> BTreeMap env } +/// Merge AnyRouter preset routing fields into the child env so launch +/// actually sends them. Claude Code reads `CLAUDE_CODE_EXTRA_BODY`. +pub fn apply_routing_env( + env: &mut BTreeMap, + routing: &crate::config::RoutingConstraints, + tool_name: &str, +) { + let Some(body) = routing.extra_body_json() else { + return; + }; + env.insert("ANYROUTER_EXTRA_BODY".into(), body.clone()); + if tool_name == "claude" { + env.insert("CLAUDE_CODE_EXTRA_BODY".into(), body); + } +} + pub fn effort_args_for(tool_name: &str, effort: Option<&str>) -> Vec { let Some(mapped) = harness_effort(tool_name, effort) else { return vec![]; @@ -488,8 +504,12 @@ pub fn claude_wants_1m(context_window: Option) -> bool { /// get the catalog id — the suffix 404s on the OpenAI-compatible API. pub fn model_id_for_tool(tool_name: &str, model: &str, context_window: Option) -> String { let id = catalog_model_id(model); - if is_auto_model(&id) { - return display_model_id(&id); + if is_auto_model(&id) || id.starts_with("anyrouter/") { + return if is_auto_model(&id) { + display_model_id(&id) + } else { + id + }; } if tool_name == "claude" && claude_wants_1m(context_window) { if id.ends_with(CLAUDE_1M_SUFFIX) { @@ -817,6 +837,14 @@ mod tests { "stealth/ox-alpha" ); assert_eq!(model_id_for_tool("claude", "auto", None), "anyrouter/auto"); + assert_eq!( + model_id_for_tool("claude", "anyrouter/auto", None), + "anyrouter/auto" + ); + assert_eq!( + model_id_for_tool("claude", "anyrouter/free", None), + "anyrouter/free" + ); } #[test] @@ -1143,4 +1171,21 @@ mod tests { Some("2048") ); } + + #[test] + fn apply_routing_env_sets_claude_extra_body() { + let mut env = BTreeMap::new(); + let mut routing = crate::config::RoutingConstraints::default(); + apply_routing_env(&mut env, &routing, "claude"); + assert!(env.get("CLAUDE_CODE_EXTRA_BODY").is_none()); + routing.set_exacto(true); + routing.set_require_tools(true); + routing.set_require_1m(true); + apply_routing_env(&mut env, &routing, "claude"); + let body = env.get("CLAUDE_CODE_EXTRA_BODY").expect("extra body"); + assert!(body.contains("\"sort\":\"exacto\""), "{body}"); + assert!(body.contains("\"require_params\":[\"tools\"]"), "{body}"); + assert!(body.contains("\"min_context\":1000000"), "{body}"); + assert_eq!(env.get("ANYROUTER_EXTRA_BODY"), Some(body)); + } } diff --git a/src/tui/state.rs b/src/tui/state.rs index f1d2dc4..19ed9b1 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -434,15 +434,51 @@ impl PaletteState { let Some(agent) = self.focused_agent.clone() else { return; }; + // Launch-row detail already carries per-agent routing flags + // (`exacto` / `tools` / `1M`). Read those so toggles do not copy + // the previous agent's on/off when focus moves. + let launch_detail = self + .entries + .iter() + .find(|e| e.group == "launch" && e.label == agent) + .map(|e| e.detail.clone()) + .unwrap_or_default(); + let exacto_on = launch_flag(&launch_detail, "exacto"); + let tools_on = launch_flag(&launch_detail, "tools"); + let ctx_on = launch_flag(&launch_detail, "1M"); let group = format!("configure · {agent}"); for entry in &mut self.entries { - let kind = configure_kind(&entry.action, &entry.label); - let Some(kind) = kind else { + let Some(kind) = configure_kind(&entry.action, &entry.label) else { continue; }; entry.group = group.clone(); - entry.action = format!("Switch {kind} {agent}"); - entry.detail = format!("for {agent}"); + match kind { + ConfigureKind::Model => { + entry.action = format!("Switch model {agent}"); + entry.detail = format!("for {agent}"); + } + ConfigureKind::Account => { + entry.action = format!("Switch account {agent}"); + entry.detail = format!("for {agent}"); + } + ConfigureKind::Key => { + entry.action = format!("Switch key {agent}"); + entry.detail = format!("for {agent}"); + } + ConfigureKind::Exacto => { + entry.action = format!("Toggle exacto {agent}"); + entry.detail = + format!("{} · for {agent}", if exacto_on { "on" } else { "off" }); + } + ConfigureKind::Tools => { + entry.action = format!("Toggle tools {agent}"); + entry.detail = format!("{} · for {agent}", if tools_on { "on" } else { "off" }); + } + ConfigureKind::MinContext => { + entry.action = format!("Toggle 1m {agent}"); + entry.detail = format!("{} · for {agent}", if ctx_on { "on" } else { "off" }); + } + } } } @@ -502,6 +538,10 @@ impl PaletteState { } } +fn launch_flag(detail: &str, flag: &str) -> bool { + detail.split('·').any(|part| part.trim() == flag) +} + fn launch_id_from_action(action: &str) -> Option { let rest = action.strip_prefix("Launch ")?.trim(); if rest.is_empty() || rest == "coding agent…" { @@ -510,16 +550,32 @@ fn launch_id_from_action(action: &str) -> Option { Some(rest.to_string()) } -/// model / account / key — the inline home-TUI switches (not install/config/quit). -fn configure_kind(action: &str, label: &str) -> Option<&'static str> { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConfigureKind { + Model, + Account, + Key, + Exacto, + Tools, + MinContext, +} + +/// model / account / key / routing toggles — inline home-TUI switches. +fn configure_kind(action: &str, label: &str) -> Option { let a = action.to_ascii_lowercase(); let l = label.to_ascii_lowercase(); if a.starts_with("switch model") || l.starts_with("model") { - Some("model") + Some(ConfigureKind::Model) } else if a.starts_with("switch account") || l.starts_with("account") { - Some("account") + Some(ConfigureKind::Account) } else if a.starts_with("switch key") || l.starts_with("key") { - Some("key") + Some(ConfigureKind::Key) + } else if a.starts_with("toggle exacto") || l.starts_with("exacto") { + Some(ConfigureKind::Exacto) + } else if a.starts_with("toggle tools") || l.starts_with("tools") { + Some(ConfigureKind::Tools) + } else if a.starts_with("toggle 1m") || l.starts_with("1m") { + Some(ConfigureKind::MinContext) } else { None } @@ -769,6 +825,61 @@ mod tests { assert_eq!(s.entries[1].group, "launch"); } + #[test] + fn palette_retargets_routing_toggles_without_dropping_launch() { + let mut s = PaletteState::new( + vec!["account duyet".into()], + vec![ + PaletteEntry::new( + "claude", + "anyrouter/auto · exacto · tools · 1M", + "launch", + "Launch claude", + ), + PaletteEntry::new("codex", "anyrouter/free", "launch", "Launch codex"), + PaletteEntry::new( + "model…", + "for claude", + "configure · claude", + "Switch model claude", + ), + PaletteEntry::new( + "exacto", + "on · for claude", + "configure · claude", + "Toggle exacto claude", + ), + PaletteEntry::new( + "tools", + "on · for claude", + "configure · claude", + "Toggle tools claude", + ), + PaletteEntry::new( + "1M ctx", + "on · for claude", + "configure · claude", + "Toggle 1m claude", + ), + ], + ); + assert!(s.entries.iter().any(|e| e.group == "launch")); + assert!(s.entries.iter().any(|e| e.group.starts_with("configure"))); + let exacto = s.entries.iter().find(|e| e.label == "exacto").unwrap(); + assert_eq!(exacto.detail, "on · for claude"); + s.apply(Action::Down); // codex — flags come from that agent's launch row + assert_eq!(s.focused_agent.as_deref(), Some("codex")); + let exacto = s.entries.iter().find(|e| e.label == "exacto").unwrap(); + assert_eq!(exacto.action, "Toggle exacto codex"); + assert_eq!(exacto.detail, "off · for codex"); + let tools = s.entries.iter().find(|e| e.label == "tools").unwrap(); + assert_eq!(tools.detail, "off · for codex"); + let ctx = s.entries.iter().find(|e| e.label == "1M ctx").unwrap(); + assert_eq!(ctx.detail, "off · for codex"); + assert!(s.entries.iter().any(|e| e.action == "Launch claude")); + assert!(s.entries.iter().any(|e| e.action == "Launch codex")); + } + #[test] fn palette_moving_to_grok_retargets_model_without_dropping_launch() { let mut s = sample_palette(); diff --git a/src/tui/view.rs b/src/tui/view.rs index f83c44c..f64e586 100644 --- a/src/tui/view.rs +++ b/src/tui/view.rs @@ -701,6 +701,8 @@ pub fn item_icon(label: &str) -> &'static str { "⬇ " } else if l.contains("model") { "◆ " + } else if l.contains("exacto") || l.contains("1m") || stem == "tools" { + "◇ " } else { "· " } @@ -1117,6 +1119,9 @@ mod tests { ("pi", "⚡"), ("model…", "◆"), ("key…", "🔑"), + ("exacto", "◇"), + ("tools", "◇"), + ("1M ctx", "◇"), ("install…", "⬇"), ("agent…", "⚡"), ] { diff --git a/tests/cli.rs b/tests/cli.rs index 11e6a3b..d04e83f 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2041,7 +2041,10 @@ agents: assert_eq!(out.status.code().unwrap_or(1), 0, "{stdout}{stderr}"); assert!(stdout.contains("LAUNCH"), "{stdout}"); assert!(stdout.contains("CONFIGURE"), "{stdout}"); - assert!(stdout.contains("for claude") || stdout.contains("configure · claude"), "{stdout}"); + assert!( + stdout.contains("for claude") || stdout.contains("configure · claude"), + "{stdout}" + ); assert!(stdout.contains("stealth/ox-alpha"), "{stdout}"); assert!(stdout.contains("grok-4"), "{stdout}"); assert!( @@ -2077,3 +2080,134 @@ fn claude_model_1m_flag_still_launches() { "{stdout}" ); } + +#[test] +fn menu_dump_shows_routing_toggles_under_configure() { + let dir = std::env::temp_dir().join(format!("anyr-cli-routing-dump-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.yaml"); + std::fs::write( + &path, + "\ +active_profile: default +last_tool: claude +profiles: + default: + api_key: sk-ar-v1-routing-secret-abcdef + default_model: auto +agents: + claude: + default_model: anyrouter/auto + provider: + sort: exacto + require_params: tools + min_context: 1000000 + grok: + default_model: anyrouter/free +", + ) + .unwrap(); + let out = anyr() + .args(["menu", "--dump-tui", "--config", path.to_str().unwrap()]) + .env("ANYR_AGENTS", "claude,grok") + .output() + .expect("menu dump routing"); + 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, "{stdout}{stderr}"); + assert!(stdout.contains("LAUNCH"), "{stdout}"); + assert!(stdout.contains("CONFIGURE"), "{stdout}"); + let launch_at = stdout.find("LAUNCH").expect("LAUNCH"); + let configure_at = stdout.find("CONFIGURE").expect("CONFIGURE"); + assert!( + launch_at < configure_at, + "CONFIGURE must not flash over LAUNCH:\n{stdout}" + ); + assert!(stdout.contains("exacto"), "{stdout}"); + assert!(stdout.contains("tools"), "{stdout}"); + assert!( + stdout.contains("1M ctx") || stdout.contains("1M"), + "{stdout}" + ); + assert!(stdout.contains("anyrouter/auto"), "{stdout}"); + assert!(stdout.contains("anyrouter/free"), "{stdout}"); + assert!( + !stdout.to_ascii_lowercase().contains("most used"), + "picker/catalog must not dump most-used:\n{stdout}" + ); + assert!( + !stdout.contains("routing-secret"), + "dump must not leak full secret: {stdout}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn claude_dry_run_sends_routing_extra_body_for_auto_and_free() { + let dir = std::env::temp_dir().join(format!("anyr-cli-routing-launch-{}", 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 +agents: + claude: + default_model: anyrouter/auto + provider: + sort: exacto + require_params: tools + min_context: 1000000 +", + ) + .unwrap(); + for model in ["anyrouter/auto", "anyrouter/free"] { + let out = anyr() + .args([ + "claude", + "--dry-run", + "--yes", + "--config", + path.to_str().unwrap(), + "--model", + model, + ]) + .output() + .expect("claude dry-run routing"); + 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, "{stdout}{stderr}"); + assert!(stdout.contains("command:"), "{stdout}"); + assert!( + stdout.contains(&format!("ANTHROPIC_MODEL={model}")), + "{stdout}" + ); + assert!( + stdout.contains("CLAUDE_CODE_EXTRA_BODY="), + "must send extra body:\n{stdout}" + ); + assert!(stdout.contains("\"sort\":\"exacto\""), "{stdout}"); + assert!( + stdout.contains("\"require_params\":[\"tools\"]"), + "{stdout}" + ); + assert!(stdout.contains("\"min_context\":1000000"), "{stdout}"); + assert!(!stdout.contains("sk-ar-v1-fixture-key-0001"), "{stdout}"); + } + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn models_dump_tui_pins_anyrouter_auto_without_most_used() { + let (code, stdout, stderr) = run(&["models", "--dump-tui"]); + assert_eq!(code, 0, "{stdout}{stderr}"); + assert!(stdout.contains("anyrouter/auto"), "{stdout}"); + assert!( + !stdout.to_ascii_lowercase().contains("most used"), + "{stdout}" + ); +} From c6780694567db029a507239a6864cc49bcc64759 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 13:03:24 +0000 Subject: [PATCH 2/2] test(verify-anyr): capture routing-filter TUI and dry-run proof Isolated control-anyr drive for #43: menu dump with exacto/tools/1M ctx, anyrouter/auto picker, claude --dry-run for auto and free extra-body fields, and a PTY exacto toggle that persists per agent. Co-authored-by: duyet --- .../artifacts/routing-filters/PROOF.md | 49 ++++++++++++++ .../artifacts/routing-filters/claude-auto.txt | 21 ++++++ .../routing-filters/claude-auto.txt.err | 0 .../routing-filters/claude-auto.txt.exit | 1 + .../artifacts/routing-filters/claude-free.txt | 22 +++++++ .../routing-filters/claude-free.txt.err | 0 .../routing-filters/claude-free.txt.exit | 1 + .../config-after-pty-toggle.yaml | 66 +++++++++++++++++++ .../routing-filters/config-after-toggles.yaml | 21 ++++++ .../artifacts/routing-filters/config-dump.txt | 26 ++++++++ .../routing-filters/config-dump.txt.err | 0 .../routing-filters/config-dump.txt.exit | 1 + .../artifacts/routing-filters/doctor.txt | 10 +++ .../routing-filters/menu-dump-after.txt | 26 ++++++++ .../routing-filters/menu-dump-after.txt.err | 0 .../routing-filters/menu-dump-after.txt.exit | 1 + .../artifacts/routing-filters/menu-dump.txt | 26 ++++++++ .../routing-filters/menu-dump.txt.err | 0 .../routing-filters/menu-dump.txt.exit | 1 + .../artifacts/routing-filters/models-dump.txt | 5 ++ .../routing-filters/models-dump.txt.err | 0 .../routing-filters/models-dump.txt.exit | 1 + .../routing-filters/pane-filter-exacto.txt | 21 ++++++ .../routing-filters/pane-focus-codex.txt | 24 +++++++ .../artifacts/routing-filters/pane-home.txt | 24 +++++++ 25 files changed, 347 insertions(+) create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/PROOF.md create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/config-after-pty-toggle.yaml create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/config-after-toggles.yaml create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/doctor.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/pane-filter-exacto.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/pane-focus-codex.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/routing-filters/pane-home.txt diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/PROOF.md b/.cursor/skills/verify-anyr/artifacts/routing-filters/PROOF.md new file mode 100644 index 0000000..c152441 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/PROOF.md @@ -0,0 +1,49 @@ +# Proof: routing-filters (#43) + +Feature: `routing-filters` +Entry points driven: `anyr menu --dump-tui`, `anyr models --dump-tui`, `anyr config --dump-tui`, `anyr claude --dry-run --yes --model anyrouter/auto`, `anyr claude --dry-run --yes --model anyrouter/free`, PTY `anyr menu` (focus + exacto toggle) +Harness: `.cursor/skills/verify-anyr/control-anyr` (`launch`, `doctor`, `cli`, `test`, `pty`, `cleanup`) +Binary: `target/debug/anyr` from this checkout (`0.1.11`) +Isolated home: `/tmp/anyr-verify-20260903-130039-7117/home` (removed by cleanup) + +## Commands + +```bash +.cursor/skills/verify-anyr/control-anyr launch +.cursor/skills/verify-anyr/control-anyr doctor +.cursor/skills/verify-anyr/control-anyr test -- --lib --test cli +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/menu-dump.txt -- menu --dump-tui +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/models-dump.txt -- models --dump-tui +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/config-dump.txt -- config --dump-tui +# write agents.claude provider.sort / require_params / min_context (and pin anyrouter/auto) +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/menu-dump-after.txt -- menu --dump-tui +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/claude-auto.txt -- claude --dry-run --yes --model anyrouter/auto +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/routing-filters/claude-free.txt -- claude --dry-run --yes --model anyrouter/free +.cursor/skills/verify-anyr/control-anyr pty start -- menu +.cursor/skills/verify-anyr/control-anyr pty send Down # retarget configure to codex +.cursor/skills/verify-anyr/control-anyr pty send Up +.cursor/skills/verify-anyr/control-anyr pty send --literal 'exa' +.cursor/skills/verify-anyr/control-anyr pty send Enter # toggle claude exacto off +.cursor/skills/verify-anyr/control-anyr pty stop +.cursor/skills/verify-anyr/control-anyr cleanup +``` + +## Results + +| Artifact | Exit | Observable | +| --- | --- | --- | +| `menu-dump.txt` | 0 | `LAUNCH` before `CONFIGURE`. Rows `exacto` / `tools` / `1M ctx` (off). No `most used`. Key masked. | +| `models-dump.txt` | 0 | Picker pins `anyrouter/auto`. No `most used`. | +| `config-dump.txt` | 0 | Settings chrome; default model `anyrouter/auto`. | +| `config-after-toggles.yaml` | — | `agents.claude` has `provider.sort: exacto`, `require_params: tools`, `min_context: 1000000`. `agents.codex` unchanged (`default_model: auto` only). | +| `menu-dump-after.txt` | 0 | Claude `anyrouter/auto` plus truncated exacto flags. CONFIGURE `on · for claude`. `LAUNCH` still above `CONFIGURE`. | +| `claude-auto.txt` | 0 | `command: claude`, `ANTHROPIC_MODEL=anyrouter/auto`, `CLAUDE_CODE_EXTRA_BODY={"min_context":1000000,"provider":{"sort":"exacto"},"require_params":["tools"]}`. Key redacted. Did not spawn Claude. | +| `claude-free.txt` | 0 | Same extra body. `ANTHROPIC_MODEL=anyrouter/free`. Did not spawn Claude. | +| `pane-home.txt` | PTY | Live home: `LAUNCH` + `CONFIGURE · CLAUDE` together. exacto/tools/1M `on · for claude`. | +| `pane-focus-codex.txt` | PTY | Down retargets to `CONFIGURE · CODEX` with `off · for codex` (does not copy claude's on). Launch list still visible. | +| `pane-filter-exacto.txt` | PTY | Typeahead `exa` highlights the exacto row; launch rows remain on screen. | +| `config-after-pty-toggle.yaml` | — | Enter cleared `provider.sort` on claude. `require_params` / `min_context` remain. Codex sibling unchanged. | + +`cargo test --locked --lib --test cli`: 186 lib + 68 cli tests passed. + +No `anyr claude` without `--dry-run`. No invented catalog ids. ox-alpha not used on the launch path (`--model anyrouter/auto` and `anyrouter/free` only). diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt new file mode 100644 index 0000000..139001b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt @@ -0,0 +1,21 @@ +command: claude +args: [] +env: +ANTHROPIC_AUTH_TOKEN=sk-ar-...-key +ANTHROPIC_BASE_URL=https://anyrouter.dev/api +ANTHROPIC_DEFAULT_FABLE_MODEL=anthropic/claude-fable-5 +ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION=Fable alias + fallback +ANTHROPIC_DEFAULT_HAIKU_MODEL=anthropic/claude-haiku-4.5 +ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION=Background & subagents +ANTHROPIC_DEFAULT_OPUS_MODEL=anthropic/claude-opus-4.6 +ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION=Opus alias +ANTHROPIC_DEFAULT_SONNET_MODEL=anthropic/claude-sonnet-4.6 +ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION=Sonnet alias +ANTHROPIC_MODEL=anyrouter/auto +ANYROUTER_EXTRA_BODY={"min_context":1000000,"provider":{"sort":"exacto"},"require_params":["tools"]} +ANYROUTER_MODEL_MODE=auto +ANYROUTER_PINNED_PRESET=@preset/coding-stack +API_TIMEOUT_MS=3000000 +CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 +CLAUDE_CODE_EXTRA_BODY={"min_context":1000000,"provider":{"sort":"exacto"},"require_params":["tools"]} +CLAUDE_CODE_SUBAGENT_MODEL=anthropic/claude-haiku-4.5 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-auto.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt new file mode 100644 index 0000000..3423979 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt @@ -0,0 +1,22 @@ +command: claude +args: [] +env: +ANTHROPIC_AUTH_TOKEN=sk-ar-...-key +ANTHROPIC_BASE_URL=https://anyrouter.dev/api +ANTHROPIC_DEFAULT_FABLE_MODEL=anyrouter/free +ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION=Fable alias + fallback +ANTHROPIC_DEFAULT_HAIKU_MODEL=anyrouter/free +ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION=Background & subagents +ANTHROPIC_DEFAULT_OPUS_MODEL=anyrouter/free +ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION=Opus alias +ANTHROPIC_DEFAULT_SONNET_MODEL=anyrouter/free +ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION=Sonnet alias +ANTHROPIC_MODEL=anyrouter/free +ANYROUTER_EXTRA_BODY={"min_context":1000000,"provider":{"sort":"exacto"},"require_params":["tools"]} +ANYROUTER_MODEL_MODE=concrete +ANYROUTER_PINNED_PRESET=@preset/coding-stack +API_TIMEOUT_MS=3000000 +CLAUDE_CODE_AUTO_COMPACT_WINDOW=1000000 +CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 +CLAUDE_CODE_EXTRA_BODY={"min_context":1000000,"provider":{"sort":"exacto"},"require_params":["tools"]} +CLAUDE_CODE_SUBAGENT_MODEL=anyrouter/free diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/claude-free.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-pty-toggle.yaml b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-pty-toggle.yaml new file mode 100644 index 0000000..eebde6b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-pty-toggle.yaml @@ -0,0 +1,66 @@ +active_profile: default +auto_update: false +channel: stable +profiles: + default: + api_key: sk-ar-v1-verify-fixture-not-a-real-key + default_model: auto + work: + api_key: sk-ar-v1-verify-work-fixture-not-a-real-key + default_model: anthropic/claude-sonnet-4.6 +agents: + claude: + default_model: anyrouter/auto + require_params: tools + min_context: 1000000 + codex: + default_model: auto +tools: + claude: + command: claude + base_url_env: ANTHROPIC_BASE_URL + auth_env: ANTHROPIC_AUTH_TOKEN + model_env: ANTHROPIC_MODEL + base_suffix: "" + enable_gateway_model_discovery: true + shadow_env: ANTHROPIC_API_KEY + codex: + command: codex + base_url_env: OPENAI_BASE_URL + auth_env: OPENAI_API_KEY + model_env: OPENAI_MODEL + base_suffix: /v1 + enable_gateway_model_discovery: false + shadow_env: OPENAI_API_KEY + grok: + command: grok + base_url_env: GROK_MODELS_BASE_URL + auth_env: GROK_CODE_XAI_API_KEY + model_env: null + base_suffix: /v1 + enable_gateway_model_discovery: false + shadow_env: null + opencode: + command: opencode + base_url_env: OPENAI_BASE_URL + auth_env: OPENAI_API_KEY + model_env: OPENAI_MODEL + base_suffix: /v1 + enable_gateway_model_discovery: false + shadow_env: null + pi: + command: pi + base_url_env: OPENAI_BASE_URL + auth_env: ANYROUTER_API_KEY + model_env: null + base_suffix: /v1 + enable_gateway_model_discovery: false + shadow_env: null + pool: + command: pool + base_url_env: POOLSIDE_STANDALONE_BASE_URL + auth_env: POOLSIDE_API_KEY + model_env: POOLSIDE_STANDALONE_MODEL + base_suffix: /v1 + enable_gateway_model_discovery: false + shadow_env: OPENAI_API_KEY diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-toggles.yaml b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-toggles.yaml new file mode 100644 index 0000000..f5aaf2b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-after-toggles.yaml @@ -0,0 +1,21 @@ +# Verification scaffolding. Copied into an isolated ANYROUTER_HOME by +# control-anyr launch. Not a live AnyRouter credential. +active_profile: default +auto_update: false +channel: stable +profiles: + default: + api_key: sk-ar-v1-verify-fixture-not-a-real-key + default_model: auto + work: + api_key: sk-ar-v1-verify-work-fixture-not-a-real-key + default_model: anthropic/claude-sonnet-4.6 +agents: + claude: + default_model: anyrouter/auto + provider: + sort: exacto + require_params: tools + min_context: 1000000 + codex: + default_model: auto diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt new file mode 100644 index 0000000..34ff9f2 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt @@ -0,0 +1,26 @@ + ╭ ▲ Config ────────────────────────────────────────────────────────╮ + │account default │ + │credits - │ + │file /tmp/anyr-verify-20260903-130039-7117/home/config.yaml │ + │ │ + │[general] claude codex grok opencode pi pool │ + │ │ + ├──────────────────────────────────────────────────────────────────┤ + │ │ + │ ACCOUNT │ + │◆ account default │ + │ api key sk-ar-v1-verif…-key │ + │ │ + │ MODEL │ + │ default anyrouter/auto │ + │ │ + │ AGENT │ + │ coding agent claude │ + │ on PATH claude, codex │ + │ │ + │ GENERAL │ + │ auto-update disabled │ + │ update channel stable │ + ├──────────────────────────────────────────────────────────────────┤ + │tab agent ↑↓ move ↵ edit x reset q close │ + ╰──────────────────────────────────────────────────────────────────╯ diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/config-dump.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/doctor.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/doctor.txt new file mode 100644 index 0000000..dbe0200 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/doctor.txt @@ -0,0 +1,10 @@ +ok binary /workspace/target/debug/anyr +ok version 0.1.11 (built 2026-09-03 13:01:14) +ok help anyr --help names CORE COMMANDS / LAUNCH +ok isolated ANYROUTER_HOME=/tmp/anyr-verify-20260903-130039-7117/home +ok config /tmp/anyr-verify-20260903-130039-7117/home/config.yaml +ok config_path config path=/tmp/anyr-verify-20260903-130039-7117/home/config.yaml +ok whoami whoami shows masked default profile +ok update_guard ANYR_NO_UPDATE=1 ANYR_NO_CATALOG=1 +ok pty stopped +ok paid_guard do not run anyr claude/codex/… without --dry-run diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt new file mode 100644 index 0000000..6e2e43f --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt @@ -0,0 +1,26 @@ + ╭ anyr ────────────────────────────────────────────────────────────────╮ + │ ▄▄ ▄▄▄ account default │ + │ ▄█▀▀█▄▄█▀ credits - │ + │ ▀▀ ▀▀▀▀ │ + ├──────────────────────────────────────────────────────────────────────┤ + │❯ █ │ + ├──────────────────────────────────────────────────────────────────────┤ + │ LAUNCH │ + │◆ ⚡ claude anyrouter/auto · default · sk-ar-v1-verif…-key · exa…│ + │ ⚡ codex anyrouter/auto · default · sk-ar-v1-verif…-key │ + │ │ + │ CONFIGURE · CLAUDE │ + │ ◆ model… for claude │ + │ ⇄ account… for claude │ + │ 🔑 key… for claude │ + │ ◇ exacto on · for claude │ + │ ◇ tools on · for claude │ + │ ◇ 1M ctx on · for claude │ + │ │ + │ MORE │ + │ ⬇ install… install a coding agent │ + │ ⚙ config… accounts · keys · agent │ + │ ✕ quit esc works too │ + ├──────────────────────────────────────────────────────────────────────┤ + │type to filter ↑↓ pick agent ↵ launch / switch esc quit │ + ╰──────────────────────────────────────────────────────────────────────╯ diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump-after.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt new file mode 100644 index 0000000..2909141 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt @@ -0,0 +1,26 @@ + ╭ anyr ────────────────────────────────────────────────────────────────╮ + │ ▄▄ ▄▄▄ account default │ + │ ▄█▀▀█▄▄█▀ credits - │ + │ ▀▀ ▀▀▀▀ │ + ├──────────────────────────────────────────────────────────────────────┤ + │❯ █ │ + ├──────────────────────────────────────────────────────────────────────┤ + │ LAUNCH │ + │◆ ⚡ claude stealth/ox-alpha · default · sk-ar-v1-verif…-key │ + │ ⚡ codex anyrouter/auto · default · sk-ar-v1-verif…-key │ + │ │ + │ CONFIGURE · CLAUDE │ + │ ◆ model… for claude │ + │ ⇄ account… for claude │ + │ 🔑 key… for claude │ + │ ◇ exacto off · for claude │ + │ ◇ tools off · for claude │ + │ ◇ 1M ctx off · for claude │ + │ │ + │ MORE │ + │ ⬇ install… install a coding agent │ + │ ⚙ config… accounts · keys · agent │ + │ ✕ quit esc works too │ + ├──────────────────────────────────────────────────────────────────────┤ + │type to filter ↑↓ pick agent ↵ launch / switch esc quit │ + ╰──────────────────────────────────────────────────────────────────────╯ diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/menu-dump.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt new file mode 100644 index 0000000..9af2e2b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt @@ -0,0 +1,5 @@ +▲ Default model +search: +──────────────────────────────────────────────── +◆ anyrouter/auto +type to search ↑↓ move ↵ select esc cancel diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.err b/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.exit b/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/models-dump.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-filter-exacto.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-filter-exacto.txt new file mode 100644 index 0000000..562bf5a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-filter-exacto.txt @@ -0,0 +1,21 @@ + + + + ┌ anyr ────────────────────────────────────────────────────────────────┐ + │ │ + │ ▄▄ ▄▄▄ account default │ + │ ▄█▀▀█▄▄█▀ credits (unknown) │ + │ ▀▀ ▀▀▀▀ │ + │ ──────────────────────────────────────────────────────────────────── │ + │ ❯ exa█ │ + │ │ + │ CONFIGURE · CLAUDE │ + │ ❯ ◇ exacto on · for claude │ + │ │ + │ LAUNCH │ + │ ⚡ claude anyrouter/auto · default · sk-ar-v1-verif…-key · e │ + │ ⚡ codex anyrouter/auto · default · sk-ar-v1-verif…-ke │ + │ │ + │ type to filter ↑↓ pick agent ↵ launch / switch esc quit │ + │ │ + └──────────────────────────────────────────────────────────────────────┘ diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-focus-codex.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-focus-codex.txt new file mode 100644 index 0000000..0dfb9e9 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-focus-codex.txt @@ -0,0 +1,24 @@ + ┌ anyr ────────────────────────────────────────────────────────────────┐ + │ │ + │ ▄▄ ▄▄▄ account default │ + │ ▄█▀▀█▄▄█▀ credits - │ + │ ▀▀ ▀▀▀▀ │ + │ ──────────────────────────────────────────────────────────────────── │ + │ ❯ █ │ + │ │ + │ LAUNCH │ + │ ⚡ claude anyrouter/auto · default · sk-ar-v1-verif…-key · e │ + │ ❯ ⚡ codex anyrouter/auto · default · sk-ar-v1-verif…-ke │ + │ │ + │ CONFIGURE · CODEX │ + │ ◆ model… for codex │ + │ ⇄ account… for codex │ + │ 🔑 key… for code │ + │ ◇ exacto off · for codex │ + │ ◇ tools off · for codex │ + │ ◇ 1M ctx off · for codex │ + │ │ + │ │ + │ type to filter ↑↓ pick agent ↵ launch / switch esc quit │ + │ │ + └──────────────────────────────────────────────────────────────────────┘ diff --git a/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-home.txt b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-home.txt new file mode 100644 index 0000000..232a3f2 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/routing-filters/pane-home.txt @@ -0,0 +1,24 @@ + ┌ anyr ────────────────────────────────────────────────────────────────┐ + │ │ + │ ▄▄ ▄▄▄ account default │ + │ ▄█▀▀█▄▄█▀ credits - │ + │ ▀▀ ▀▀▀▀ │ + │ ──────────────────────────────────────────────────────────────────── │ + │ ❯ █ │ + │ │ + │ LAUNCH │ + │ ❯ ⚡ claude anyrouter/auto · default · sk-ar-v1-verif…-key · e │ + │ ⚡ codex anyrouter/auto · default · sk-ar-v1-verif…-ke │ + │ │ + │ CONFIGURE · CLAUDE │ + │ ◆ model… for claude │ + │ ⇄ account… for claude │ + │ 🔑 key… for claud │ + │ ◇ exacto on · for claude │ + │ ◇ tools on · for claude │ + │ ◇ 1M ctx on · for claude │ + │ │ + │ │ + │ type to filter ↑↓ pick agent ↵ launch / switch esc quit │ + │ │ + └──────────────────────────────────────────────────────────────────────┘