From 774c209657acf7a036ddc68c0d0cd2a987f3bf3b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 06:03:32 +0000 Subject: [PATCH 1/2] fix(tui): pin anyrouter/auto in the inline model picker The model picker was filling from a usage-sorted catalog and labeling auto as "most used", so anyrouter/auto could not be selected. Pin that preset first, sort remaining catalog ids by name, and dump the picker with --dump-tui. Co-authored-by: Duyet Le Co-authored-by: duyetbot --- .cursor/skills/verify-anyr/features/README.md | 1 + .../verify-anyr/features/model-picker.md | 37 +++ src/commands.rs | 258 +++++++++++++++--- src/help.rs | 5 + src/spawn.rs | 20 +- src/tui/state.rs | 18 ++ tests/cli.rs | 125 +++++++++ 7 files changed, 417 insertions(+), 47 deletions(-) create mode 100644 .cursor/skills/verify-anyr/features/model-picker.md diff --git a/.cursor/skills/verify-anyr/features/README.md b/.cursor/skills/verify-anyr/features/README.md index 7b65ccb..99bfa2a 100644 --- a/.cursor/skills/verify-anyr/features/README.md +++ b/.cursor/skills/verify-anyr/features/README.md @@ -51,5 +51,6 @@ Keep implementation details out of the map. Name only user paths, stable handles - [Onboard prompts](./onboard-prompts.md) covers paste-ready impl/plan/fix prompts and JSON output without network. - [Agent launch dry-run](./agent-launch-dry-run.md) covers printing the child command and redacted env without spawning an agent. - [TUI launcher](./tui-launcher.md) covers `menu --dump-tui`, `config --dump-tui`, and optional PTY quit. +- [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. diff --git a/.cursor/skills/verify-anyr/features/model-picker.md b/.cursor/skills/verify-anyr/features/model-picker.md new file mode 100644 index 0000000..98e8435 --- /dev/null +++ b/.cursor/skills/verify-anyr/features/model-picker.md @@ -0,0 +1,37 @@ +# Model picker catalog + +The inline model picker lets a user select the `anyrouter/auto` preset without filling the list from a most-used / usage ranking. Typeahead filters the same inline list. No extra screen. + +## Sub-features + +- `picker-dump` prints one model-picker frame that includes `anyrouter/auto` and does not say `most used`. +- `picker-use-auto` persists `anyrouter/auto` from `models use` without a catalog fetch. +- `ox-alpha-yolo` keeps `anyr claude --model stealth/ox-alpha[1m] --yolo --dry-run` working. + +## How to get to it (user POV) + +- Run `anyr models --dump-tui` (or `anyr models --pick --dump-tui`) to print the picker frame. +- From the launcher, choose `model…` (same inline picker; TTY). +- From settings, edit the default model row (same inline picker; TTY). +- Run `anyr models use anyrouter/auto` to pin the preset from the CLI. +- Type `anyrouter/auto` in the picker search to select the preset. + +## Driving it with control-anyr + +Preconditions: + +- `control-anyr doctor` is clean. +- Isolated config contains the fixture `default` profile. +- Helper sets `ANYR_NO_CATALOG=1`, so the dump is the picker catalog (preset only), not a live usage list. + +- **Picker dump.** Run `control-anyr cli --out artifacts/model-picker/dump.txt -- models --dump-tui`. Exit code `0`. Stdout contains `anyrouter/auto`. Stdout does not contain `most used`. Stdout has no ESC (`\x1b`). +- **Pin the preset.** Run `control-anyr cli --out artifacts/model-picker/use-auto.txt -- models use anyrouter/auto`. Exit code `0`. Stdout contains `anyrouter/auto`. Isolated `config.yaml` no longer pins a concrete catalog id as `default_model` (auto stays unset / preset). +- **Ox-alpha yolo (dry).** Run `control-anyr cli --out artifacts/model-picker/ox-alpha-yolo.txt -- claude --dry-run --yes --key sk-ar-v1-fixture-key-0001 --model stealth/ox-alpha[1m] --yolo`. Exit code `0`. Stdout contains `ANTHROPIC_MODEL=stealth/ox-alpha[1m]` and `"--dangerously-skip-permissions"`. Stdout does not contain the full fixture key. +- **Proof.** Keep `dump.txt` and `use-auto.txt`. They show the picker data source can set `anyrouter/auto` without a most-used dump. + +## Gotchas + +- `ANYR_NO_CATALOG=1` is expected isolation. The dump then shows the pinned preset, not a live catalog. That is the proof that the picker is not a usage ranking dump. +- `--dump-tui` is not a TTY session. Do not claim keybindings work from the dump alone; use `pty` only if you need to type the query. +- Do not invent other model ids. `anyrouter/auto` is the real preset. Concrete ids like `stealth/ox-alpha` stay CLI `--model` paths. +- `--yolo` without `--dry-run` starts Claude. Skip that. diff --git a/src/commands.rs b/src/commands.rs index 3c53881..165b09f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -276,7 +276,7 @@ fn allowed_flags(command: &str) -> Option<&'static [&'static str]> { ], "models" => &[ "profile", "config", "json", "key", "base-url", "pick", "haiku", "sonnet", "opus", - "fable", "agent", + "fable", "agent", "dump-tui", ], "usage" => &["profile", "base-url", "config", "json", "key", "no-detail"], "whoami" => &["profile", "config", "json"], @@ -695,11 +695,10 @@ fn persist_login( let mut cfg = upsert_profile(existing.unwrap_or_default(), &name, profile); cfg.active_profile = name.clone(); if !parsed.flag_true("yes") && term::is_interactive() { - if let Ok(models) = fetch_models(&base, Some(&key)) { - if let Ok(id) = pick_model(&models, None, "Default model") { - if let Some(p) = cfg.profiles.get_mut(&name) { - p.default_model = Some(id); - } + let models = models_for_picker(&base, Some(&key), env); + if let Ok(id) = pick_model(&models, None, "Default model") { + if let Some(p) = cfg.profiles.get_mut(&name) { + set_model_slot(p, "default", id); } } let tools = ["claude", "codex", "grok", "opencode", "pi", "pool"]; @@ -746,11 +745,10 @@ fn run_login(parsed: &ParsedArgs, env: &BTreeMap) -> Result String { if is_auto_model(id) { - match most_used_model_id(models) { - Some(top) => format!("auto · most used · {top}"), - None => "auto · most used".into(), - } - } else if models + // The selectable preset id — never a "most used" ranking stand-in. + return display_model_id(id); + } + if models .iter() .find(|m| catalog_model_id(&m.id) == id) .and_then(|m| m.context_length) @@ -762,18 +760,71 @@ fn model_pick_label(id: &str, models: &[CatalogModel]) -> String { } } +/// Picker catalog: pin `anyrouter/auto`, then real catalog ids in name order. +/// Usage-sorted fetch order is not the picker contents. fn pick_ids(models: &[CatalogModel]) -> Vec { - let mut ids = Vec::new(); - ids.push("auto".into()); - for model in models { - let id = catalog_model_id(&model.id); - if !id.is_empty() && !is_auto_model(&id) && !ids.iter().any(|existing| existing == &id) { + let mut ids = vec!["anyrouter/auto".into()]; + let mut catalog: Vec = models + .iter() + .map(|m| catalog_model_id(&m.id)) + .filter(|id| !id.is_empty() && !is_auto_model(id)) + .collect(); + catalog.sort(); + catalog.dedup(); + for id in catalog { + if !ids.iter().any(|existing| existing == &id) { ids.push(id); } } ids } +fn picker_choice_list(models: &[CatalogModel]) -> (Vec, Vec) { + let ids = pick_ids(models); + let labels = ids.iter().map(|id| model_pick_label(id, models)).collect(); + (ids, labels) +} + +/// Catalog rows for the inline picker. Live fetch is optional: the preset +/// `anyrouter/auto` is always selectable even when lookup is off or fails. +fn models_for_picker( + base: &str, + key: Option<&str>, + env: &BTreeMap, +) -> Vec { + if !catalog_lookup_enabled(env) { + return Vec::new(); + } + fetch_models(base, key).unwrap_or_default() +} + +fn render_model_picker_dump( + models: &[CatalogModel], + current: Option<&str>, + env: &BTreeMap, +) -> String { + let (ids, labels) = picker_choice_list(models); + let current_id = current.map(catalog_model_id); + let shown_idx = current_id.and_then(|id| { + ids.iter() + .position(|s| s == &id || (is_auto_model(&id) && is_auto_model(s))) + }); + #[cfg(feature = "native")] + { + crate::tui::dump_pick( + "Default model", + &labels, + shown_idx, + crate::tui::dump_cols(env), + ) + } + #[cfg(not(feature = "native"))] + { + let _ = (env, shown_idx); + format!("{}\n", labels.join("\n")) + } +} + fn pick_list( title: &str, header: &[String], @@ -796,19 +847,18 @@ fn pick_model( current: Option<&str>, title: &str, ) -> Result { - let ids = pick_ids(models); + let (ids, labels) = picker_choice_list(models); if ids.is_empty() { return Err("No models in catalog.".into()); } let current_id = current.map(catalog_model_id); - let labels: Vec = ids.iter().map(|id| model_pick_label(id, models)).collect(); let shown_idx = current_id.and_then(|id| { ids.iter() .position(|s| s == &id || (is_auto_model(&id) && is_auto_model(s))) }); let idx = pick_list( title, - &["type to search, enter to pin · x in config clears to auto".into()], + &["type to search · anyrouter/auto is pinned · enter to select".into()], &labels, shown_idx, )?; @@ -1033,7 +1083,6 @@ fn run_models(parsed: &ParsedArgs, env: &BTreeMap) -> Result) -> Result) -> Result --agent ", )); @@ -1112,24 +1205,6 @@ fn run_models(parsed: &ParsedArgs, env: &BTreeMap) -> Result Result) -> CatalogModel { + CatalogModel { + id: id.into(), + name: None, + owned_by: None, + context_length, + } + } + + #[test] + fn pick_ids_pins_anyrouter_auto_and_does_not_dump_usage_order() { + // Input order is a usage ranking (most-used first). + let models = vec![ + cm("stealth/ox-alpha", Some(1_000_000)), + cm("openai/gpt-5.4-mini", Some(128_000)), + cm("anthropic/claude-sonnet-4.6", Some(200_000)), + cm("auto", None), + ]; + let ids = pick_ids(&models); + assert_eq!(ids[0], "anyrouter/auto"); + assert_eq!( + ids, + vec![ + "anyrouter/auto", + "anthropic/claude-sonnet-4.6", + "openai/gpt-5.4-mini", + "stealth/ox-alpha", + ] + ); + assert_ne!(ids[1], "stealth/ox-alpha", "must not lead with most-used"); + } + + #[test] + fn pick_ids_empty_catalog_still_has_preset() { + assert_eq!(pick_ids(&[]), vec!["anyrouter/auto"]); + } + + #[test] + fn model_pick_label_shows_preset_id_not_most_used() { + let models = vec![cm("stealth/ox-alpha", Some(1_000_000))]; + let label = model_pick_label("anyrouter/auto", &models); + assert_eq!(label, "anyrouter/auto"); + assert!(!label.contains("most used"), "{label}"); + assert!(!label.contains("stealth/ox-alpha"), "{label}"); + assert_eq!( + model_pick_label("stealth/ox-alpha", &models), + "stealth/ox-alpha · 1M" + ); + } + + #[cfg(feature = "native")] + #[test] + fn picker_typeahead_selects_anyrouter_auto() { + use crate::tui::{drive_picker, Action, Outcome, PickerState}; + let models = vec![ + cm("stealth/ox-alpha", Some(1_000_000)), + cm("openai/gpt-5.4-mini", None), + ]; + let (ids, labels) = picker_choice_list(&models); + assert_eq!(ids[0], "anyrouter/auto"); + assert_eq!(labels[0], "anyrouter/auto"); + assert!( + labels.iter().all(|l| !l.contains("most used")), + "{labels:?}" + ); + let mut state = PickerState::new("Default model", labels, Some(0)); + let typed: Vec = "anyrouter/auto".chars().map(Action::Char).collect(); + let out = drive_picker(&mut state, &typed); + assert_eq!(out, Outcome::Continue); + assert_eq!(state.filtered()[0].1, "anyrouter/auto"); + assert_eq!(state.apply(Action::Enter), Outcome::Selected(0)); + assert_eq!(ids[0], "anyrouter/auto"); + } + + #[cfg(feature = "native")] + #[test] + fn picker_dump_leads_with_preset_not_usage_ranking() { + let models = vec![ + cm("stealth/ox-alpha", Some(1_000_000)), + cm("openai/gpt-5.4-mini", None), + ]; + let env = BTreeMap::new(); + let frame = render_model_picker_dump(&models, Some("auto"), &env); + assert!(frame.contains("anyrouter/auto"), "{frame}"); + assert!(!frame.contains("most used"), "{frame}"); + let auto_at = frame.find("anyrouter/auto").expect("preset"); + let usage_at = frame.find("stealth/ox-alpha"); + if let Some(usage_at) = usage_at { + assert!( + auto_at < usage_at, + "preset must appear before catalog dump:\n{frame}" + ); + } + } +} diff --git a/src/help.rs b/src/help.rs index b585d5e..3cfa359 100644 --- a/src/help.rs +++ b/src/help.rs @@ -331,16 +331,21 @@ Usage: {bin} models use --agent {bin} models use --haiku|--sonnet|--opus|--fable {bin} models --pick + {bin} models --dump-tui Lists every model id usable with --model. `use` / `--pick` persist the session default, or Claude Code's opus / sonnet / haiku / fable aliases. `--agent` pins the launch model for that coding agent only (the id must already be known — this command does not invent catalog ids). +The inline picker pins `anyrouter/auto` as a selectable preset. Type to +filter catalog ids. It does not dump a most-used / usage ranking. + Options: --json Print as JSON --pick Interactive picker (TTY) — default / haiku / sonnet / opus / fable --agent Pin the model on one coding agent instead of the session default + --dump-tui Print one picker frame (pins anyrouter/auto) and exit --haiku Persist Claude haiku alias --sonnet Persist Claude sonnet alias --opus Persist Claude opus alias diff --git a/src/spawn.rs b/src/spawn.rs index b5dc3e3..664e328 100644 --- a/src/spawn.rs +++ b/src/spawn.rs @@ -470,13 +470,9 @@ pub fn display_model_id(model: &str) -> String { } } -/// Launcher / settings label: auto means "pick most used", not a stored id. +/// Launcher / settings label. Auto is the selectable preset `anyrouter/auto`. pub fn session_model_label(model: &str) -> String { - if is_auto_model(model) { - "auto · most used".into() - } else { - catalog_model_id(model) - } + display_model_id(model) } pub fn claude_wants_1m(context_window: Option) -> bool { @@ -823,6 +819,18 @@ mod tests { assert_eq!(model_id_for_tool("claude", "auto", None), "anyrouter/auto"); } + #[test] + fn session_model_label_is_anyrouter_auto_preset() { + assert_eq!(session_model_label("auto"), "anyrouter/auto"); + assert_eq!(session_model_label("anyrouter/auto"), "anyrouter/auto"); + assert_eq!(session_model_label(""), "anyrouter/auto"); + assert_eq!( + session_model_label("stealth/ox-alpha[1m]"), + "stealth/ox-alpha" + ); + assert_ne!(session_model_label("auto"), "auto · most used"); + } + #[test] fn redact_auth_token_and_keep_numeric() { assert_eq!( diff --git a/src/tui/state.rs b/src/tui/state.rs index cff983e..84e184a 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -503,6 +503,24 @@ mod tests { assert_eq!(s.apply(Action::Esc), Outcome::Cancelled); } + #[test] + fn picker_typeahead_selects_anyrouter_auto_preset() { + let mut s = PickerState::new( + "Default model", + vec![ + "anyrouter/auto".into(), + "stealth/ox-alpha · 1M".into(), + "openai/gpt-5.4-mini".into(), + ], + Some(1), + ); + for c in "anyrouter/auto".chars() { + assert_eq!(s.apply(Action::Char(c)), Outcome::Continue); + } + assert_eq!(s.filtered()[0].1, "anyrouter/auto"); + assert_eq!(s.apply(Action::Enter), Outcome::Selected(0)); + } + #[test] fn menu_wraps_and_selects() { let mut s = MenuState::new( diff --git a/tests/cli.rs b/tests/cli.rs index ee6b9d7..aa1339e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -557,6 +557,42 @@ fn claude_yolo_flag_expands_to_dangerously_skip_permissions() { ); } +#[test] +fn claude_yolo_with_ox_alpha_1m_still_works() { + let key = "sk-ar-v1-fixture-key-0001"; + let (code, stdout, stderr) = { + let out = anyr() + .args([ + "claude", + "--dry-run", + "--yes", + "--key", + key, + "--model", + "stealth/ox-alpha[1m]", + "--yolo", + ]) + .env("ANYROUTER_HOME", temp_home()) + .env_remove("ANYROUTER_API_KEY") + .output() + .expect("dry-run"); + ( + out.status.code().unwrap_or(1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) + }; + assert_eq!(code, 0, "stderr={stderr}"); + assert!( + stdout.contains("ANTHROPIC_MODEL=stealth/ox-alpha[1m]"), + "{stdout}" + ); + assert!( + stdout.contains("\"--dangerously-skip-permissions\""), + "{stdout}" + ); +} + #[test] fn codex_yolo_flag_is_accepted_but_not_forwarded() { let key = "sk-ar-v1-fixture-key-0001"; @@ -1147,6 +1183,95 @@ profiles: let _ = std::fs::remove_dir_all(&dir); } +#[test] +fn models_dump_tui_pins_anyrouter_auto_without_usage_dump() { + let dir = std::env::temp_dir().join(format!("anyr-cli-models-dump-{}", 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-models-dump-secret-value-abcdef + default_model: auto +", + ) + .unwrap(); + let out = anyr() + .args(["models", "--dump-tui", "--config", path.to_str().unwrap()]) + .output() + .expect("models dump"); + 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('\u{1b}'), + "dump must be ANSI-free: {stdout}" + ); + assert!(stdout.contains("anyrouter/auto"), "{stdout}"); + assert!( + !stdout.contains("most used"), + "picker must not dump a most-used ranking:\n{stdout}" + ); + let auto_at = stdout.find("anyrouter/auto").expect("preset"); + for ranked in ["stealth/ox-alpha", "openai/gpt", "claude-sonnet"] { + if let Some(at) = stdout.find(ranked) { + assert!( + auto_at < at, + "anyrouter/auto must lead, not a usage dump:\n{stdout}" + ); + } + } + assert!( + !stdout.contains("models-dump-secret-value"), + "dump must not leak full secret: {stdout}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn models_use_anyrouter_auto_persists_without_network() { + let dir = std::env::temp_dir().join(format!("anyr-cli-models-auto-{}", 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-one + default_model: stealth/ox-alpha +", + ) + .unwrap(); + let out = anyr() + .args([ + "models", + "use", + "anyrouter/auto", + "--config", + path.to_str().unwrap(), + "--base-url", + "http://127.0.0.1:9", + ]) + .env_remove("ANYROUTER_API_KEY") + .output() + .expect("models use auto"); + 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("anyrouter/auto"), "{stdout}"); + let cfg = std::fs::read_to_string(&path).expect("config"); + assert!( + !cfg.contains("default_model: stealth/ox-alpha"), + "preset should clear the pinned catalog id:\n{cfg}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn logout_clears_key_and_does_not_print_it() { let dir = std::env::temp_dir().join(format!("anyr-cli-logout-{}", std::process::id())); From da9245030bf42e42548dd7c1bbfa172ef94e64c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 06:05:51 +0000 Subject: [PATCH 2/2] test(verify-anyr): capture model picker anyrouter/auto proof Dump, models use, launch dry-run, and ox-alpha --yolo artifacts showing the picker catalog is the preset, not a most-used ranking. Co-authored-by: Duyet Le Co-authored-by: duyetbot --- .../artifacts/model-picker/PROOF.md | 38 +++++++++++++++++++ .../artifacts/model-picker/dump.txt | 5 +++ .../artifacts/model-picker/dump.txt.err | 0 .../artifacts/model-picker/dump.txt.exit | 1 + .../artifacts/model-picker/launch-auto.txt | 19 ++++++++++ .../model-picker/launch-auto.txt.err | 0 .../model-picker/launch-auto.txt.exit | 1 + .../artifacts/model-picker/menu-after.txt | 24 ++++++++++++ .../artifacts/model-picker/menu-after.txt.err | 0 .../model-picker/menu-after.txt.exit | 1 + .../artifacts/model-picker/ox-alpha-yolo.txt | 20 ++++++++++ .../model-picker/ox-alpha-yolo.txt.err | 0 .../model-picker/ox-alpha-yolo.txt.exit | 1 + .../artifacts/model-picker/use-auto.txt | 1 + .../artifacts/model-picker/use-auto.txt.err | 0 .../artifacts/model-picker/use-auto.txt.exit | 1 + 16 files changed, 112 insertions(+) create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/PROOF.md create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/dump.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.exit create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.err create mode 100644 .cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.exit diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/PROOF.md b/.cursor/skills/verify-anyr/artifacts/model-picker/PROOF.md new file mode 100644 index 0000000..ea5b9f7 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/PROOF.md @@ -0,0 +1,38 @@ +# Proof: model-picker + +Feature: `model-picker` +Entry points driven: `anyr models --dump-tui`, `anyr models use anyrouter/auto`, `anyr claude --dry-run --yes --model stealth/ox-alpha[1m] --yolo`, `anyr claude --dry-run --yes` after pinning auto, `anyr menu --dump-tui` +Harness: `.cursor/skills/verify-anyr/control-anyr` (`launch`, `doctor`, `cli`, `cleanup`) +Binary: `target/debug/anyr` from this checkout (`0.1.11`) +Isolated home: `/tmp/anyr-verify-20260903-060433-6302/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 cli --out artifacts/model-picker/dump.txt -- models --dump-tui +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/model-picker/use-auto.txt -- models use anyrouter/auto +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/model-picker/launch-auto.txt -- claude --dry-run --yes --key sk-ar-v1-fixture-key-0001 +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/model-picker/ox-alpha-yolo.txt -- claude --dry-run --yes --key sk-ar-v1-fixture-key-0001 --model 'stealth/ox-alpha[1m]' --yolo +.cursor/skills/verify-anyr/control-anyr cli --out artifacts/model-picker/menu-after.txt -- menu --dump-tui +.cursor/skills/verify-anyr/control-anyr cleanup +``` + +## Results + +| Artifact | Exit | Observable | +| --- | --- | --- | +| `dump.txt` | 0 | picker frame with `◆ anyrouter/auto`; no `most used`; no usage-ranked catalog dump | +| `use-auto.txt` | 0 | `Saved default model anyrouter/auto` | +| `launch-auto.txt` | 0 | `ANTHROPIC_MODEL=anyrouter/auto`; `command:`; key redacted | +| `ox-alpha-yolo.txt` | 0 | `ANTHROPIC_MODEL=stealth/ox-alpha[1m]` and `"--dangerously-skip-permissions"` | +| `menu-after.txt` | 0 | `model anyrouter/auto` on the launcher; no `most used` | + +Complementary suite: `models_dump_tui_pins_anyrouter_auto_without_usage_dump`, `models_use_anyrouter_auto_persists_without_network`, `claude_yolo_with_ox_alpha_1m_still_works`, `picker_catalog_tests::*` passed. + +No `anyr claude` without `--dry-run`. No paid tokens. + +## Cleanup + +`control-anyr cleanup` removed the isolated workdir and left this `artifacts/model-picker/` tree in place. diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt b/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt new file mode 100644 index 0000000..9af2e2b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/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/model-picker/dump.txt.err b/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.exit b/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/dump.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt b/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt new file mode 100644 index 0000000..87c11a3 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt @@ -0,0 +1,19 @@ +command: claude +args: [] +env: +ANTHROPIC_AUTH_TOKEN=sk-ar-...0001 +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_MODEL_MODE=auto +ANYROUTER_PINNED_PRESET=@preset/coding-stack +API_TIMEOUT_MS=3000000 +CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 +CLAUDE_CODE_SUBAGENT_MODEL=anthropic/claude-haiku-4.5 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.err b/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.exit b/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/launch-auto.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt b/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt new file mode 100644 index 0000000..db56468 --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt @@ -0,0 +1,24 @@ + ╭ anyr ────────────────────────────────────────────────────────────────╮ + │ ▄█▄▀█████████▄ account default │ + │ ▄█████▄ ███ key sk-ar-v1-verif…-key │ + │ ▄███▀ ▀██▄ ▄▄▄███▀ model anyrouter/auto │ + │ ▄███▀ ▀██▄▀███▄ agent claude │ + │▄███▀ ▀██▄▀███▄ credits - │ + ├──────────────────────────────────────────────────────────────────────┤ + │❯ █ │ + ├──────────────────────────────────────────────────────────────────────┤ + │ LAUNCH │ + │◆ ⚡ claude anyrouter/auto │ + │ ⚡ codex Codex │ + │ │ + │ CONFIGURE │ + │ ◆ model… switch session default │ + │ ⚡ agent… switch default agent │ + │ ⇄ account… switch profile │ + │ 🔑 key… switch API key │ + │ ⬇ install… install a coding agent │ + │ ⚙ config… accounts · keys · agent │ + │ ✕ quit esc works too │ + ├──────────────────────────────────────────────────────────────────────┤ + │type to filter ↑↓ move ↵ run esc quit │ + ╰──────────────────────────────────────────────────────────────────────╯ diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.err b/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.exit b/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/menu-after.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt b/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt new file mode 100644 index 0000000..69d63aa --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt @@ -0,0 +1,20 @@ +command: claude +args: ["--dangerously-skip-permissions"] +env: +ANTHROPIC_AUTH_TOKEN=sk-ar-...0001 +ANTHROPIC_BASE_URL=https://anyrouter.dev/api +ANTHROPIC_DEFAULT_FABLE_MODEL=stealth/ox-alpha[1m] +ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION=Fable alias + fallback +ANTHROPIC_DEFAULT_HAIKU_MODEL=stealth/ox-alpha[1m] +ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION=Background & subagents +ANTHROPIC_DEFAULT_OPUS_MODEL=stealth/ox-alpha[1m] +ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION=Opus alias +ANTHROPIC_DEFAULT_SONNET_MODEL=stealth/ox-alpha[1m] +ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION=Sonnet alias +ANTHROPIC_MODEL=stealth/ox-alpha[1m] +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_SUBAGENT_MODEL=stealth/ox-alpha[1m] diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.err b/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.exit b/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/ox-alpha-yolo.txt.exit @@ -0,0 +1 @@ +0 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt b/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt new file mode 100644 index 0000000..7af855b --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt @@ -0,0 +1 @@ +Saved default model anyrouter/auto diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.err b/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.err new file mode 100644 index 0000000..e69de29 diff --git a/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.exit b/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.cursor/skills/verify-anyr/artifacts/model-picker/use-auto.txt.exit @@ -0,0 +1 @@ +0