From 6de4ae3a5d58868ac011d8bc6dd1eb50d37e1b7e Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 23:54:49 +0700 Subject: [PATCH 1/4] Re-verify the claude and codex catalogues against the installed CLIs codex-cli 0.156.1 (`codex debug models`) adds gpt-6-sol and gpt-6-luna; gpt-6-astra stays the default. claude 2.1.267 answers `fable` as claude-fable-5-1 and `haiku` as claude-haiku-4-5-20251001, and reports a 1M window for claude-opus-5. claude-opus-5-5 is rejected by 2.1.267 (`unrecognized_model`), so it is not listed. Codex verified_version moves to 0.156.1; the live codex suite passes on it apart from the two tests that need a session fixture. --- README.md | 13 ++++--- src/agent.rs | 4 +- src/model.rs | 103 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7df04e6..e9a57df 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Every entry point that spawns a CLI (`run`, `stream`, `interrupt`, `Probe::run`, ## What each agent can actually do -Verified live, against `claude 2.1.267`, `codex-cli 0.154.0`, `GitHub Copilot CLI 1.0.88` and +Verified live, against `claude 2.1.267`, `codex-cli 0.156.1`, `GitHub Copilot CLI 1.0.88` and `grok 1.0.40`, and not inferred from documentation. | | session id | fork | events | system prompt | resume flag | @@ -187,7 +187,8 @@ the account actually allows. `Model::is_default` marks the safe pre-selection. Aliases and pinned ids are both carried, and `Model::kind` tells them apart, because they do not always agree. On claude 2.1.212, `--model opus` reported `claude-opus-4-8` while `--model claude-opus-5` reported `claude-opus-5`, even though that release's own notes call -Opus 5 the default Opus model. An alias is whatever the account resolves it to. +Opus 5 the default Opus model. On 2.1.267 they agree again: `opus` reports `claude-opus-5` +and `fable` reports `claude-fable-5-1`. An alias is whatever the account resolves it to. ### Reasoning effort @@ -571,10 +572,10 @@ own variables do not reach the child. cleanly, reports `subtype: "success"`, and puts "There's an issue with the selected model" where the answer belongs. Codex does the same and wraps the upstream body in a JSON string. Both come back as `Error::AgentError`; see below. -- **`claude-opus-5` defaults to a 200k window while every other 5-series model is 1M.** - Verified by running each id: `claude-sonnet-5` and `claude-fable-5` report a 1,000,000 - token window natively, `claude-opus-5` reports 200,000 and needs the `[1m]` suffix - (`claude-opus-5[1m]`) to widen. The catalogue carries both forms. +- **`claude-opus-5`'s window depends on the CLI release.** On 2.1.212 it reported 200,000 + and needed the `[1m]` suffix (`claude-opus-5[1m]`) to widen, while `claude-sonnet-5` and + `claude-fable-5` reported 1,000,000 natively. On 2.1.267 plain `claude-opus-5` reports + 1,000,000 too. The catalogue carries both forms, and `default` resolves to the suffixed one. - **Claude's `stream-json` requires `--verbose`**, or it refuses to start. Handled. - **Large prompts move to stdin automatically** above 128 KiB, so a long prompt never fails with `E2BIG`. diff --git a/src/agent.rs b/src/agent.rs index 983b366..614c355 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -399,8 +399,8 @@ impl Agent { let (major, minor, patch) = match self { // `claude --version` -> "2.1.267 (Claude Code)" Agent::Claude => (2, 1, 267), - // `codex --version` -> "codex-cli 0.154.0" - Agent::Codex => (0, 154, 0), + // `codex --version` -> "codex-cli 0.156.1" + Agent::Codex => (0, 156, 1), // `copilot --version` -> "GitHub Copilot CLI 1.0.88." Agent::Copilot => (1, 0, 88), // `grok --version` -> "grok 1.0.40 (eb1a2256660d)" diff --git a/src/model.rs b/src/model.rs index 837c15e..0106d39 100644 --- a/src/model.rs +++ b/src/model.rs @@ -70,7 +70,7 @@ pub struct Model { /// /// Kept as strings for the same reason ids are, and the three agents make /// the case on their own: Claude documents five levels, Copilot seven, and - /// Codex varies them per model, offering `ultra` on its two frontier models + /// Codex varies them per model, offering `ultra` on its larger models /// and not on the rest. A shared enum would have to be edited before a new /// level could even be named. /// @@ -156,19 +156,20 @@ impl Agent { #[must_use] pub fn models_verified(&self) -> Verified { match self { - // Mixed: the five aliases were read from the `/model` picker, but - // the pinned ids and the `best` / `opusplan` / `[1m]` entries come - // from documentation. `source` records the weakest evidence behind - // any entry, since that is the one a reader needs to distrust. + // Mixed: the aliases were read from the `/model` picker and every + // entry was run on 2.1.267, but the notes come from documentation + // and Claude cannot list its models headlessly. `source` records + // the weakest evidence behind any entry, since that is the one a + // reader needs to distrust. Agent::Claude => Verified { source: Source::Docs, - checked: "2026-07-30", - against: "claude 2.1.212", + checked: "2026-09-23", + against: "claude 2.1.267", }, Agent::Codex => Verified { source: Source::Cli, checked: "2026-09-23", - against: "codex-cli 0.154.0", + against: "codex-cli 0.156.1", }, // Read from the `/model` picker. Copilot has no headless list; see // `discover_models`. @@ -207,7 +208,7 @@ impl Agent { Agent::Codex => discover_codex(self.bin(), reactor).await, Agent::Grok => discover_grok(self.bin(), reactor).await, // Neither can be asked without a terminal, verified against - // Copilot CLI 1.0.75 and claude 2.1.212. Copilot has no `models` + // Copilot CLI 1.0.75 and claude 2.1.267. Copilot has no `models` // subcommand, rejects an unknown `--model` without listing the valid // ones, and its ACP `session/new` reply carries session modes and // permissions but no models. Claude documents its aliases in @@ -228,9 +229,9 @@ impl Agent { /// and respect what the account is entitled to, which a pinned id does neither /// of. Pinned ids follow for a caller who needs one exact model. /// -/// Verified against claude 2.1.212 (`--help`) and the published model list, -/// 2026-07-29. -/// Claude's effort levels, verified from `claude --help` on 2.1.212: +/// Verified against claude 2.1.267 (`--help`, and a one-line run of every entry) +/// on 2026-09-23. The notes still come from the published model list. +/// Claude's effort levels, verified from `claude --help` on 2.1.267: /// `--effort ` (low, medium, high, xhigh, max). /// /// Session-level rather than per-model, so every entry carries the same set: @@ -311,8 +312,10 @@ fn claude_aliases() -> Vec { // terminal record reports `contextWindow: 1000000`, keyed by the // suffixed id (`claude-sonnet-5[1m]`). The suffix also composes with a // pinned id: `claude-opus-5[1m]` ran and reported 1M. `fable[1m]` is - // accepted too, resolving to plain `claude-fable-5` at 1M, since Fable - // is 1M natively and needs no suffix. + // accepted too (2.1.212), resolving to plain `claude-fable-5` at 1M, since Fable + // is 1M natively and needs no suffix. On 2.1.267 (2026-09-23) `opus[1m]` and + // `sonnet[1m]` were run again and reported 1M under `claude-opus-5[1m]` and + // `claude-sonnet-5[1m]`; `fable[1m]` was not re-run. Model::new( "opus[1m]", "Opus (1M context)", @@ -336,21 +339,33 @@ fn claude_aliases() -> Vec { /// /// Its own subtitle says so: "For other/previous model names, specify with /// `--model`". They are still worth carrying, because an alias and a pinned id -/// do not always agree. Verified on 2026-07-29 against claude 2.1.212 by running -/// both: `--model opus` reported `claude-opus-4-8` in its usage while -/// `--model claude-opus-5` reported `claude-opus-5`, even though that release's -/// own notes call Opus 5 "now the default Opus model". An alias is whatever the -/// account resolves it to, which is not always the newest model. +/// do not always agree. On claude 2.1.212 (2026-07-29) `--model opus` reported +/// `claude-opus-4-8` in its usage while `--model claude-opus-5` reported +/// `claude-opus-5`, even though that release's own notes call Opus 5 "now the +/// default Opus model". On 2.1.267 (2026-09-23) the two agree again: `opus` +/// reports `claude-opus-5`, `sonnet` reports `claude-sonnet-5`, `fable` and +/// `best` report `claude-fable-5-1`, and `haiku` reports +/// `claude-haiku-4-5-20251001`. An alias is whatever the account resolves it +/// to, which is not always the newest model. +/// +/// Every id below was run on claude 2.1.267 (2026-09-23) and answered under its +/// own name. `claude-opus-5-5` was tried and refused as +/// `[claude-code:unrecognized_model]`, and 2.1.267 carries no such string, so +/// it is not catalogued. `claude-fable-5` is left out: it is still accepted, +/// but its run produced no output of its own and the answer came from +/// `claude-opus-4-8`, and both the `fable` alias and `best` now resolve to +/// `claude-fable-5-1`. `claude-haiku-4-5` is replaced by the dated id the +/// `haiku` alias reports. fn claude_pinned() -> Vec { vec![ - // Windows verified by running each id on claude 2.1.212 (2026-07-30). - // `claude-opus-5` is the odd one out: every other 5-series model is 1M - // natively, while it defaults to 200k and needs the suffix. Both forms - // are catalogued so a picker can offer the choice explicitly. + // Windows as reported by running each id on claude 2.1.267 + // (2026-09-23). On 2.1.212 plain `claude-opus-5` reported 200k and + // needed the suffix; on 2.1.267 it reports 1,000,000 on its own. The + // suffixed form stays because it is what `default` resolves to. Model::new( "claude-opus-5", "Claude Opus 5", - "For complex agentic coding and enterprise work (200k context)", + "For complex agentic coding and enterprise work (1M context)", Kind::Pinned, CLAUDE_EFFORTS, false, @@ -358,7 +373,7 @@ fn claude_pinned() -> Vec { Model::new( "claude-opus-5[1m]", "Claude Opus 5 (1M context)", - "Opus 5 with a 1M token context window", + "Opus 5 with the 1M context window requested explicitly", Kind::Pinned, CLAUDE_EFFORTS, false, @@ -372,17 +387,17 @@ fn claude_pinned() -> Vec { false, ), Model::new( - "claude-fable-5", - "Claude Fable 5", + "claude-fable-5-1", + "Claude Fable 5.1", "Next-generation intelligence for long-running agents (1M context)", Kind::Pinned, CLAUDE_EFFORTS, false, ), Model::new( - "claude-haiku-4-5", + "claude-haiku-4-5-20251001", "Claude Haiku 4.5", - "The fastest model with near-frontier intelligence", + "The fastest model with near-frontier intelligence (200k context)", Kind::Pinned, CLAUDE_EFFORTS, false, @@ -392,11 +407,11 @@ fn claude_pinned() -> Vec { /// Codex, in the priority order the CLI itself reports. /// -/// Verified by running `codex debug models` against codex-cli 0.154.0 on +/// Verified by running `codex debug models` against codex-cli 0.156.1 on /// 2026-09-23. `gpt-reserve` and `codex-auto-review` are reported with /// `visibility: "hide"` and are left out for that reason; [`discover_codex`] -/// applies the same filter. `gpt-daybreak-blue-latest` is listed only in the -/// server-refreshed catalogue (the bundled one hides it), so it is +/// applies the same filter. `gpt-daybreak-blue-latest` was listed only in the +/// server-refreshed catalogue on 0.154.0 (the bundled one hid it), so it is /// account-dependent. `gpt-5.5` carries an upgrade notice retiring it on /// 2026-10-14 in favour of `gpt-5.6-sol`. The descriptions are Codex's own. fn codex_models() -> Vec { @@ -412,6 +427,22 @@ fn codex_models() -> Vec { FULL, true, ), + Model::new( + "gpt-6-sol", + "GPT-6-Sol", + "Workhorse model for coding and everyday work.", + Kind::Pinned, + FULL, + false, + ), + Model::new( + "gpt-6-luna", + "GPT-6-Luna", + "Fast and affordable model for easier tasks.", + Kind::Pinned, + TO_MAX, + false, + ), Model::new( "gpt-5.6-sol", "GPT-5.6-Sol", @@ -867,7 +898,7 @@ mod tests { ); } - /// Verified from `claude --help` (2.1.212) and `copilot --help` (1.0.75). + /// Verified from `claude --help` (2.1.267) and `copilot --help` (1.0.75). /// Copilot's set is two wider at the bottom, which is the whole reason /// levels are strings rather than a shared enum. #[test] @@ -903,11 +934,11 @@ mod tests { .collect() }; assert!( - by_id("gpt-5.6-sol").contains(&"ultra".to_string()), - "its frontier model offers ultra" + by_id("gpt-6-sol").contains(&"ultra".to_string()), + "its workhorse model offers ultra" ); assert!( - !by_id("gpt-5.6-luna").contains(&"ultra".to_string()), + !by_id("gpt-6-luna").contains(&"ultra".to_string()), "its fast model does not" ); } From e33fbe601579bbf64c830298b07bf64fba1ee2a8 Mon Sep 17 00:00:00 2001 From: meh Date: Thu, 24 Sep 2026 00:12:54 +0700 Subject: [PATCH 2/4] Catalogue Claude Opus 5.5, verified on claude 2.1.280 claude 2.1.280 accepts claude-opus-5-5 and claude-opus-5-5[1m], both 1M, and resolves `opus` and `default` to claude-opus-5-5. verified_version moves to 2.1.280; its --help differs from 2.1.267 only in the wording of --bare and --safe-mode. The live claude suite passes on it. README: 2.1.280 accepts an unknown --effort with a stderr warning and runs at the default, and Request::effort does not validate, so that is a silent downgrade today. Recorded as a gotcha. --- README.md | 10 +++++++--- src/agent.rs | 4 ++-- src/model.rs | 41 ++++++++++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e9a57df..3d93092 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Every entry point that spawns a CLI (`run`, `stream`, `interrupt`, `Probe::run`, ## What each agent can actually do -Verified live, against `claude 2.1.267`, `codex-cli 0.156.1`, `GitHub Copilot CLI 1.0.88` and +Verified live, against `claude 2.1.280`, `codex-cli 0.156.1`, `GitHub Copilot CLI 1.0.88` and `grok 1.0.40`, and not inferred from documentation. | | session id | fork | events | system prompt | resume flag | @@ -187,7 +187,7 @@ the account actually allows. `Model::is_default` marks the safe pre-selection. Aliases and pinned ids are both carried, and `Model::kind` tells them apart, because they do not always agree. On claude 2.1.212, `--model opus` reported `claude-opus-4-8` while `--model claude-opus-5` reported `claude-opus-5`, even though that release's own notes call -Opus 5 the default Opus model. On 2.1.267 they agree again: `opus` reports `claude-opus-5` +Opus 5 the default Opus model. On 2.1.280 `opus` reports `claude-opus-5-5` and `fable` reports `claude-fable-5-1`. An alias is whatever the account resolves it to. ### Reasoning effort @@ -575,8 +575,12 @@ own variables do not reach the child. - **`claude-opus-5`'s window depends on the CLI release.** On 2.1.212 it reported 200,000 and needed the `[1m]` suffix (`claude-opus-5[1m]`) to widen, while `claude-sonnet-5` and `claude-fable-5` reported 1,000,000 natively. On 2.1.267 plain `claude-opus-5` reports - 1,000,000 too. The catalogue carries both forms, and `default` resolves to the suffixed one. + 1,000,000 too. The catalogue carries both forms. Opus 5.5 (2.1.280) is 1M plain and suffixed. - **Claude's `stream-json` requires `--verbose`**, or it refuses to start. Handled. +- **Claude accepts an unknown `--effort` and runs at the default.** On 2.1.280 `--effort + ultra` prints a warning to stderr and exits 0. `Request::effort` passes the level through + unvalidated, so an effort outside `Model::efforts` is a silent downgrade on Claude today. + Pick efforts from the catalogue. - **Large prompts move to stdin automatically** above 128 KiB, so a long prompt never fails with `E2BIG`. diff --git a/src/agent.rs b/src/agent.rs index 614c355..0ffaa5e 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -397,8 +397,8 @@ impl Agent { #[must_use] pub fn verified_version(self) -> crate::Version { let (major, minor, patch) = match self { - // `claude --version` -> "2.1.267 (Claude Code)" - Agent::Claude => (2, 1, 267), + // `claude --version` -> "2.1.280 (Claude Code)" + Agent::Claude => (2, 1, 280), // `codex --version` -> "codex-cli 0.156.1" Agent::Codex => (0, 156, 1), // `copilot --version` -> "GitHub Copilot CLI 1.0.88." diff --git a/src/model.rs b/src/model.rs index 0106d39..dc5dfe3 100644 --- a/src/model.rs +++ b/src/model.rs @@ -157,14 +157,14 @@ impl Agent { pub fn models_verified(&self) -> Verified { match self { // Mixed: the aliases were read from the `/model` picker and every - // entry was run on 2.1.267, but the notes come from documentation + // entry was run on 2.1.280, but the notes come from documentation // and Claude cannot list its models headlessly. `source` records // the weakest evidence behind any entry, since that is the one a // reader needs to distrust. Agent::Claude => Verified { source: Source::Docs, - checked: "2026-09-23", - against: "claude 2.1.267", + checked: "2026-09-24", + against: "claude 2.1.280", }, Agent::Codex => Verified { source: Source::Cli, @@ -342,16 +342,18 @@ fn claude_aliases() -> Vec { /// do not always agree. On claude 2.1.212 (2026-07-29) `--model opus` reported /// `claude-opus-4-8` in its usage while `--model claude-opus-5` reported /// `claude-opus-5`, even though that release's own notes call Opus 5 "now the -/// default Opus model". On 2.1.267 (2026-09-23) the two agree again: `opus` -/// reports `claude-opus-5`, `sonnet` reports `claude-sonnet-5`, `fable` and -/// `best` report `claude-fable-5-1`, and `haiku` reports +/// default Opus model". On 2.1.280 (2026-09-24) `opus` and `default` report +/// `claude-opus-5-5`, `opus[1m]` reports `claude-opus-5-5[1m]`, `sonnet` and +/// `opusplan` report `claude-sonnet-5`, and `fable` and `best` report +/// `claude-fable-5-1`. On 2.1.267 `haiku` reported /// `claude-haiku-4-5-20251001`. An alias is whatever the account resolves it /// to, which is not always the newest model. /// -/// Every id below was run on claude 2.1.267 (2026-09-23) and answered under its -/// own name. `claude-opus-5-5` was tried and refused as -/// `[claude-code:unrecognized_model]`, and 2.1.267 carries no such string, so -/// it is not catalogued. `claude-fable-5` is left out: it is still accepted, +/// Opus 5.5 ids were run on claude 2.1.280 (2026-09-24) and answered under +/// their own names; 2.1.267 refused `claude-opus-5-5` as +/// `[claude-code:unrecognized_model]`. The rest were run on 2.1.267 +/// (2026-09-23), and `claude-opus-5` again on 2.1.280, reporting 1M. +/// `claude-fable-5` is left out: it is still accepted, /// but its run produced no output of its own and the answer came from /// `claude-opus-4-8`, and both the `fable` alias and `best` now resolve to /// `claude-fable-5-1`. `claude-haiku-4-5` is replaced by the dated id the @@ -361,7 +363,24 @@ fn claude_pinned() -> Vec { // Windows as reported by running each id on claude 2.1.267 // (2026-09-23). On 2.1.212 plain `claude-opus-5` reported 200k and // needed the suffix; on 2.1.267 it reports 1,000,000 on its own. The - // suffixed form stays because it is what `default` resolves to. + // suffixed form stays for a caller that pinned it. Opus 5.5 reported + // 1,000,000 plain and suffixed on 2.1.280. + Model::new( + "claude-opus-5-5", + "Claude Opus 5.5", + "The latest Opus, for complex agentic coding (1M context)", + Kind::Pinned, + CLAUDE_EFFORTS, + false, + ), + Model::new( + "claude-opus-5-5[1m]", + "Claude Opus 5.5 (1M context)", + "Opus 5.5 with the 1M context window requested explicitly", + Kind::Pinned, + CLAUDE_EFFORTS, + false, + ), Model::new( "claude-opus-5", "Claude Opus 5", From b21663a7e337bf102f3d95acc8d0e70d613c3e59 Mon Sep 17 00:00:00 2001 From: meh Date: Thu, 24 Sep 2026 00:30:21 +0700 Subject: [PATCH 3/4] Keep dropped models as retired entries, and refuse to run them A host stores model ids (agencyzero keeps them in its database). Deleting an entry made a stored id vanish from its picker while its runs kept using it, and some dropped ids still run: claude-fable-5 is accepted on 2.1.267 and answers as claude-opus-4-8. Model gains `retired: Option` (since, replacement, reason). A dropped model stays in the catalogue with it set, is never the default, and discovery keeps it. A request naming one fails before anything is spawned with the new Error::RetiredModel, which carries the entry so a host can offer the replacement. Ids the catalogue does not know still pass through. Retired: claude-fable-5 (use claude-fable-5-1), codex gpt-5.4 and gpt-5.4-mini (no longer listed on 0.154.0; use gpt-6-astra). claude-haiku-4-5 is restored as a live entry: it still answers as itself on 2.1.280. --- README.md | 35 +++++-- src/error.rs | 24 +++++ src/lib.rs | 2 +- src/model.rs | 273 +++++++++++++++++++++++++++++++++++++++++++++---- src/request.rs | 91 ++++++++++++++++- src/run.rs | 4 + 6 files changed, 403 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3d93092..b9548cb 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,28 @@ for model in Agent::Claude.models() { } ``` -The list is **advisory and never enforced**. `Request::model` takes any string and nothing -here checks it, so a model released this morning is not blocked by a list compiled last -month. A model the account cannot reach comes back as `Error::AgentError` with the -provider's own status and wording. +The list is **advisory, with one exception**. `Request::model` takes any string and an id +the list does not know passes straight through, so a model released this morning is not +blocked by a list compiled last month. A model the account cannot reach comes back as +`Error::AgentError` with the provider's own status and wording. + +The exception is a **retired** entry. A model a CLI stops offering is kept in the list with +`Model::retired` set (`since`, `reason`, and a `replacement` where there is one) instead of +being deleted, because a host that stored the id would otherwise watch it vanish from its +picker while its runs kept using it. `Model::is_retired()` lets a picker flag it; a retired +entry is never `is_default`. A request naming one is refused with `Error::RetiredModel` +before anything is spawned. It carries the model and its `Retired`, so a host can offer the +replacement, and the user picks another model: + +| Agent | Retired id | Since | Replacement | Why | +|---|---|---|---|---| +| Claude | `claude-fable-5` | claude 2.1.267 | `claude-fable-5-1` | Still accepted, but the answer comes from `claude-opus-4-8` | +| Codex | `gpt-5.4` | codex-cli 0.154.0 | `gpt-6-astra` | No longer listed by `codex debug models` | +| Codex | `gpt-5.4-mini` | codex-cli 0.154.0 | `gpt-6-astra` | No longer listed by `codex debug models` | + +Retirement is per agent: Copilot's own `gpt-5.4` and `claude-fable-5` entries are untouched. +`claude-haiku-4-5` is not retired; it still answers under its own name and sits beside the +dated `claude-haiku-4-5-20251001` that the `haiku` alias reports. That distinction matters more than it sounds, because **a catalogue is not an entitlement**. Copilot's picker lists twenty-three models and a Free plan permits exactly one: @@ -200,8 +218,8 @@ let request = Request::new(Agent::Codex, prompt).model("gpt-5.6-sol").effort("ul Passed through verbatim, like the model, because the sets are not interchangeable: Claude documents five levels, Copilot seven, and Codex varies them per model, offering `ultra` on -its two frontier models and not on the rest. Delivered as `--effort` on Claude and Copilot, -and as `-c model_reasoning_effort=` on Codex, which has no flag for it. +its larger models and not on the rest. Delivered as `--effort` on Claude, as +`--reasoning-effort` on Copilot and Grok, and as `-c model_reasoning_effort=` on Codex, which has no flag for it. Support is not uniform even within one agent. Copilot's `auto` **exits 1** rather than ignoring the flag: @@ -225,6 +243,11 @@ returning the compiled list: both enumerate models only in an interactive picker caller asking for discovery is asking for freshness. `Agent::models_verified()` records how each compiled list was established and against which release. +Discovery still appends the compiled list's retired entries after the discovered ones, since +a CLI lists only what it offers today and a picker has to show and flag a stored id that has +gone. An id the CLI lists that the compiled list marks retired is flagged in place, not +repeated. + ### Disabling thinking `Request::thinking(false)` turns the model's reasoning off for a run; left unset the agent diff --git a/src/error.rs b/src/error.rs index 3057a64..43db8a8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -90,6 +90,30 @@ pub enum Error { message: String, }, + /// The request named a model the agent's catalogue marks retired. + /// + /// A user state, not a programming mistake: the id was valid once and a + /// host may have stored it. Refused rather than run, because a retired id + /// can still be accepted and answer as a different model. A host shows + /// this as "pick another model", offering [`crate::Retired::replacement`]. + #[error( + "{agent} no longer offers `{model}` (retired since {}: {}); {}", + retired.since, + retired.reason, + match &retired.replacement { + Some(replacement) => format!("pick `{replacement}` instead"), + None => "pick another model".to_string(), + } + )] + RetiredModel { + /// The agent that was asked. + agent: Agent, + /// The id the request named. + model: String, + /// Why it was retired, and what replaces it. + retired: crate::Retired, + }, + /// The request asked an agent for something it cannot do headlessly: /// forking on Codex, a named session on Copilot, an event stream on an /// agent that only prints text. diff --git a/src/lib.rs b/src/lib.rs index a264833..7d4ab32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ pub use auth::{AuthState, AuthStatus}; pub use command::{Command, Commands, Compaction}; pub use error::{Error, Result}; pub use event::{Event, MAX_CAPTURE, MAX_EVENT_BYTES, MAX_LINE, TRUNCATION_MARK}; -pub use model::{Kind, Model, Source, Verified}; +pub use model::{Kind, Model, Retired, Source, Verified}; pub use outcome::{Outcome, RateLimit, Stop, Usage}; pub use probe::{Probe, Version, VersionStatus}; pub use request::Request; diff --git a/src/model.rs b/src/model.rs index dc5dfe3..756ca4c 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,13 +1,26 @@ //! Which models each agent offers, so a host can render a picker. //! -//! The catalogue is **advisory and never enforced**. [`crate::Request::model`] -//! takes any string and this crate does not check it against anything here. A -//! model that shipped this morning must not be blocked by a list compiled last +//! The catalogue is **advisory, with one exception**. [`crate::Request::model`] +//! takes any string and passes an id this list does not know straight through. +//! A model that shipped this morning must not be blocked by a list compiled last //! month, and a picked model the account cannot reach fails as //! [`crate::Error::AgentError`] carrying the provider's own status and wording. //! Enforcing the list would trade a clear runtime error for a wrong compile-time //! one. //! +//! # Retired models +//! +//! The exception is an entry marked [`Model::retired`]. A model a CLI stops +//! offering is kept here as a retired entry rather than deleted, because a host +//! that stored its id (agencyzero keeps them as strings in its database) would +//! otherwise see the id vanish from its picker while its runs kept using it. +//! Retired entries are listed so a picker can show and flag them, are never +//! [`Model::is_default`], and are refused: a request naming one fails with +//! [`crate::Error::RetiredModel`] before anything is spawned, naming the reason +//! and the replacement, so the user picks another model. Some of them still run +//! (`claude-fable-5` is accepted and answers as a different model), which is +//! exactly the quiet wrong answer the refusal exists to prevent. +//! //! # A catalogue is not an entitlement //! //! What an agent *offers* and what an account may *use* are different sets, and @@ -81,10 +94,53 @@ pub struct Model { /// refused, since nothing in this crate validates against it. pub efforts: Vec>, /// Whether the agent uses this when the caller names no model. + /// Always `false` on a retired entry. pub is_default: bool, + /// Set when the CLI no longer offers this model. A request naming it is + /// refused; see [`Retired`]. + #[serde(default)] + pub retired: Option, +} + +/// Why a catalogued model may no longer be used, and what to use instead. +/// +/// A retired entry stays in the catalogue so an id a host stored keeps a name +/// and a reason in its picker, but a request naming it fails with +/// [`crate::Error::RetiredModel`] rather than running. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct Retired { + /// The CLI release it was found retired on, such as `claude 2.1.267`. + pub since: Cow<'static, str>, + /// The id to offer in its place, when there is an obvious one. + pub replacement: Option>, + /// What was observed, in one line. + pub reason: Cow<'static, str>, } impl Model { + /// Whether the CLI no longer offers this model. A retired model never runs. + #[must_use] + pub fn is_retired(&self) -> bool { + self.retired.is_some() + } + + /// Mark a catalogue entry retired. A retired entry is never the default. + fn retire( + mut self, + since: &'static str, + replacement: Option<&'static str>, + reason: &'static str, + ) -> Model { + self.is_default = false; + self.retired = Some(Retired { + since: Cow::Borrowed(since), + replacement: replacement.map(Cow::Borrowed), + reason: Cow::Borrowed(reason), + }); + self + } + /// Build a catalogue entry from static parts. fn new( id: &'static str, @@ -101,6 +157,7 @@ impl Model { kind, efforts: efforts.iter().map(|e| Cow::Borrowed(*e)).collect(), is_default, + retired: None, } } } @@ -138,10 +195,12 @@ pub enum Source { } impl Agent { - /// The models this agent offers, best first. + /// The models this agent offers, best first, followed by the ones it no + /// longer offers, marked [`Model::retired`]. /// - /// Advisory: this is not enforced, and it does not tell you what an account - /// may actually use. See [`Model`] and [`Agent::models_verified`]. + /// Advisory apart from the retired entries, which a request may not name. + /// It does not tell you what an account may actually use. See [`Model`] + /// and [`Agent::models_verified`]. #[must_use] pub fn models(&self) -> Vec { match self { @@ -152,6 +211,14 @@ impl Agent { } } + /// The compiled-in entry for `id` if it is retired, `None` for a live + /// entry or an id the catalogue does not know. + pub(crate) fn retired_model(self, id: &str) -> Option { + self.models() + .into_iter() + .find(|model| model.id == id && model.is_retired()) + } + /// How this agent's compiled-in catalogue was established, and when. #[must_use] pub fn models_verified(&self) -> Verified { @@ -195,6 +262,12 @@ impl Agent { /// `reactor` drives the child's pipes; the caller keeps its /// [`nagoya::reactor::Reactor`] alive until this returns. /// + /// The retired entries of the compiled-in catalogue are appended after the + /// discovered ones, because a CLI lists only what it offers today and a + /// picker still has to show and flag an id a host stored before it went. + /// An id the CLI lists that the catalogue marks retired is flagged in place + /// rather than repeated, since a request naming it is refused either way. + /// /// # Errors /// [`Error::Unsupported`] on an agent with no headless way to answer, which /// today is Claude and Copilot. That is deliberately an error rather than a @@ -204,9 +277,9 @@ impl Agent { /// binary is missing, [`Error::Spawn`] if it cannot be run, and /// [`Error::Parse`] if its output is not the expected shape. pub async fn discover_models(&self, reactor: &nagoya::reactor::Handle) -> Result> { - match self { - Agent::Codex => discover_codex(self.bin(), reactor).await, - Agent::Grok => discover_grok(self.bin(), reactor).await, + let discovered = match self { + Agent::Codex => discover_codex(self.bin(), reactor).await?, + Agent::Grok => discover_grok(self.bin(), reactor).await?, // Neither can be asked without a terminal, verified against // Copilot CLI 1.0.75 and claude 2.1.267. Copilot has no `models` // subcommand, rejects an unknown `--model` without listing the valid @@ -214,12 +287,39 @@ impl Agent { // permissions but no models. Claude documents its aliases in // `--help` but has no subcommand that enumerates them. In both the // interactive `/model` picker is the only listing. - Agent::Claude | Agent::Copilot => Err(Error::Unsupported { - agent: *self, - what: "listing models without a terminal", - }), + Agent::Claude | Agent::Copilot => { + return Err(Error::Unsupported { + agent: *self, + what: "listing models without a terminal", + }); + } + }; + Ok(keep_retired(discovered, self.models())) + } +} + +/// Carry the catalogue's retired entries into a discovered list. +/// +/// Split from the spawn so it can be tested without a subprocess. A discovered +/// id the catalogue marks retired takes the catalogue's [`Retired`] and loses +/// any default; the rest are appended in catalogue order. If that took the +/// default away, the first live entry becomes the default, as discovery itself +/// does when a CLI names none. +fn keep_retired(mut discovered: Vec, catalogue: Vec) -> Vec { + for entry in catalogue.into_iter().filter(Model::is_retired) { + if let Some(listed) = discovered.iter_mut().find(|m| m.id == entry.id) { + listed.is_default = false; + listed.retired = entry.retired; + } else { + discovered.push(entry); } } + if !discovered.iter().any(|m| m.is_default) + && let Some(first) = discovered.iter_mut().find(|m| !m.is_retired()) + { + first.is_default = true; + } + discovered } /// Claude, aliases first. @@ -353,11 +453,8 @@ fn claude_aliases() -> Vec { /// their own names; 2.1.267 refused `claude-opus-5-5` as /// `[claude-code:unrecognized_model]`. The rest were run on 2.1.267 /// (2026-09-23), and `claude-opus-5` again on 2.1.280, reporting 1M. -/// `claude-fable-5` is left out: it is still accepted, -/// but its run produced no output of its own and the answer came from -/// `claude-opus-4-8`, and both the `fable` alias and `best` now resolve to -/// `claude-fable-5-1`. `claude-haiku-4-5` is replaced by the dated id the -/// `haiku` alias reports. +/// `claude-fable-5` is retired, with its evidence next to the entry. +/// `claude-haiku-4-5` stays beside the dated id the `haiku` alias reports. fn claude_pinned() -> Vec { vec![ // Windows as reported by running each id on claude 2.1.267 @@ -421,6 +518,36 @@ fn claude_pinned() -> Vec { CLAUDE_EFFORTS, false, ), + // Not retired. 0.5.2 swapped it for the dated id above only because + // that is what the `haiku` alias reports on claude 2.1.267; the + // undated id itself still answers under its own name on 2.1.267 + // (2026-09-23), so a host that stored it keeps a working model. + Model::new( + "claude-haiku-4-5", + "Claude Haiku 4.5", + "The fastest model with near-frontier intelligence (200k context)", + Kind::Pinned, + CLAUDE_EFFORTS, + false, + ), + // Retired on claude 2.1.267 (2026-09-23). Still accepted by `--model`, + // but the run produced no output of its own and the answer came from + // `claude-opus-4-8`, while the `fable` alias and `best` both resolve to + // `claude-fable-5-1` (also on 2.1.280, 2026-09-24). Running it would + // be a quiet wrong answer, so it is refused. + Model::new( + "claude-fable-5", + "Claude Fable 5", + "Next-generation intelligence for long-running agents (1M context)", + Kind::Pinned, + CLAUDE_EFFORTS, + false, + ) + .retire( + "claude 2.1.267", + Some("claude-fable-5-1"), + "accepted, but the answer comes from claude-opus-4-8", + ), ] } @@ -433,6 +560,7 @@ fn claude_pinned() -> Vec { /// server-refreshed catalogue on 0.154.0 (the bundled one hid it), so it is /// account-dependent. `gpt-5.5` carries an upgrade notice retiring it on /// 2026-10-14 in favour of `gpt-5.6-sol`. The descriptions are Codex's own. +/// Models Codex has stopped listing follow the live ones, marked retired. fn codex_models() -> Vec { const FULL: &[&str] = &["low", "medium", "high", "xhigh", "max", "ultra"]; const TO_MAX: &[&str] = &["low", "medium", "high", "xhigh", "max"]; @@ -502,6 +630,36 @@ fn codex_models() -> Vec { TO_XHIGH, false, ), + // Retired on codex-cli 0.154.0 (2026-09-23): `codex debug models` + // stopped listing both, and 0.156.1 does not list them either. Listed + // on 0.146.0 (2026-08-07) with these descriptions and levels. Codex + // names no successor, so the replacement is its current default. + Model::new( + "gpt-5.4", + "GPT-5.4", + "Strong model for everyday coding.", + Kind::Pinned, + TO_XHIGH, + false, + ) + .retire( + "codex-cli 0.154.0", + Some("gpt-6-astra"), + "no longer listed by `codex debug models`", + ), + Model::new( + "gpt-5.4-mini", + "GPT-5.4-Mini", + "Small, fast, and cost-efficient model for simpler coding tasks.", + Kind::Pinned, + TO_XHIGH, + false, + ) + .retire( + "codex-cli 0.154.0", + Some("gpt-6-astra"), + "no longer listed by `codex debug models`", + ), ] } @@ -660,6 +818,7 @@ fn parse_grok_models(stdout: &str) -> Result> { kind: Kind::Pinned, efforts: GROK_EFFORTS.iter().map(|e| Cow::Borrowed(*e)).collect(), is_default, + retired: None, }); } if models.is_empty() { @@ -760,6 +919,7 @@ fn parse_codex_models(stdout: &str) -> Result> { // Codex names a default reasoning level per model but never a // default model, so the top of its own ordering stands in. is_default: false, + retired: None, }; let priority = m .get("priority") @@ -982,6 +1142,83 @@ mod tests { } } + /// A retired entry is kept for a picker, never pre-selected. + #[test] + fn no_retired_entry_is_a_default() { + for agent in [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok] { + for model in agent.models().iter().filter(|m| m.is_retired()) { + assert!( + !model.is_default, + "{agent} marks retired {} as default", + model.id + ); + } + } + } + + /// Every model a catalogue has dropped is still there, flagged, so a stored + /// id keeps its name. `claude-haiku-4-5` still answers as itself and is live. + #[test] + fn dropped_models_stay_as_retired_entries() { + let retired = |agent: Agent| -> Vec { + agent + .models() + .into_iter() + .filter(Model::is_retired) + .map(|m| m.id.into_owned()) + .collect() + }; + assert_eq!(retired(Agent::Claude), ["claude-fable-5"]); + assert_eq!(retired(Agent::Codex), ["gpt-5.4", "gpt-5.4-mini"]); + let haiku = Agent::Claude + .models() + .into_iter() + .find(|m| m.id == "claude-haiku-4-5") + .expect("claude-haiku-4-5 should be catalogued"); + assert!(!haiku.is_retired(), "claude-haiku-4-5 still runs as itself"); + let fable = Agent::Claude + .retired_model("claude-fable-5") + .expect("claude-fable-5 is retired"); + assert_eq!( + fable.retired.and_then(|r| r.replacement).as_deref(), + Some("claude-fable-5-1") + ); + } + + /// Discovery lists what a CLI offers today; the retired entries follow it, + /// after every discovered one, so a picker built from discovery can still + /// show and flag a stored id. + #[test] + fn discovery_keeps_retired_entries() { + let discovered = parse_codex_models(CODEX_OUTPUT).expect("should parse"); + let models = keep_retired(discovered, Agent::Codex.models()); + let ids: Vec<&str> = models.iter().map(|m| m.id.as_ref()).collect(); + assert_eq!(ids, ["gpt-5.6-sol", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]); + assert!(models[..2].iter().all(|m| !m.is_retired())); + assert!(models[2..].iter().all(Model::is_retired)); + assert_eq!(models.iter().filter(|m| m.is_default).count(), 1); + assert!(models[0].is_default); + } + + /// A CLI listing an id the catalogue retired gets it flagged in place, not + /// repeated, and a retired id never keeps the default. + #[test] + fn discovery_flags_a_listed_retired_id_in_place() { + let output = r#"{"models":[ + {"slug":"gpt-5.4","display_name":"GPT-5.4","visibility":"list","priority":1}, + {"slug":"gpt-6-astra","display_name":"GPT-6-Astra","visibility":"list","priority":2} + ]}"#; + let discovered = parse_codex_models(output).expect("should parse"); + let models = keep_retired(discovered, Agent::Codex.models()); + let ids: Vec<&str> = models.iter().map(|m| m.id.as_ref()).collect(); + assert_eq!(ids, ["gpt-5.4", "gpt-6-astra", "gpt-5.4-mini"]); + assert!(models[0].is_retired() && !models[0].is_default); + assert!( + models[1].is_default, + "the first live entry takes the default" + ); + } + /// The catalogue is a suggestion, not a gate. A model released after this /// list was compiled has to reach the command line untouched. #[test] diff --git a/src/request.rs b/src/request.rs index 960e650..0cab9c5 100644 --- a/src/request.rs +++ b/src/request.rs @@ -163,6 +163,11 @@ impl Request { /// Pin the model. Passed through verbatim; this crate does not validate /// model names, so an unknown one surfaces as the agent's own error. + /// + /// The one exception is an id the agent's compiled-in catalogue marks + /// [`crate::Model::retired`]: building the command line, and so starting + /// the run, fails with [`crate::Error::RetiredModel`] carrying the reason and + /// the replacement, so the user picks another model. #[must_use] pub fn model(mut self, model: impl Into) -> Self { self.model = Some(model.into()); @@ -529,7 +534,8 @@ impl Request { /// will run before they approve it. /// /// # Errors - /// [`crate::Error::Unsupported`] if the agent cannot honour this request. + /// [`crate::Error::Unsupported`] if the agent cannot honour this request, + /// and [`crate::Error::RetiredModel`] for a model its catalogue marks retired. pub fn argv(&self) -> Result> { Ok(self .typed_argv()? @@ -538,11 +544,40 @@ impl Request { .collect()) } + /// Refuse a model the agent's compiled-in catalogue marks retired. + /// + /// Only a catalogued, retired id is refused. An id the catalogue does not + /// know still passes through verbatim, since a model newer than this crate + /// must not be blocked. Refused rather than run because a retired id can + /// still be accepted and answer as a different model (`claude-fable-5` on + /// claude 2.1.267), which is a quiet wrong answer. + /// + /// # Errors + /// [`crate::Error::RetiredModel`] carrying the model, why it was retired, + /// and its replacement when there is one. + pub(crate) fn refuse_retired_model(&self) -> Result<()> { + let Some(id) = &self.model else { + return Ok(()); + }; + match self.agent.retired_model(id).and_then(|model| model.retired) { + Some(retired) => Err(crate::Error::RetiredModel { + agent: self.agent, + model: id.clone(), + retired, + }), + None => Ok(()), + } + } + /// The command line with per-argument sensitivity, the single source both /// the executable and the redacted forms are derived from. + /// + /// Every spawn builds its argv here, so the retired-model refusal sits here + /// too and nothing runs on a retired model. pub(crate) fn typed_argv(&self) -> Result> { use crate::agent::{Arg, Sensitivity}; + self.refuse_retired_model()?; let plan = self.plan(); let mut argv = self.agent.typed_argv(&plan)?; // Raw arguments have no known shape, so they are assumed to carry @@ -694,4 +729,58 @@ mod tests { let at = argv.iter().position(|a| a == "--resume").unwrap(); assert_eq!(argv[at + 1], "sess-9"); } + + /// A retired model never runs: the argv, which every spawn goes through, + /// is refused, and the message tells the user what to pick instead. + #[test] + fn a_retired_model_is_refused_with_its_replacement() { + for (agent, id, replacement) in [ + (Agent::Claude, "claude-fable-5", "claude-fable-5-1"), + (Agent::Codex, "gpt-5.4", "gpt-6-astra"), + (Agent::Codex, "gpt-5.4-mini", "gpt-6-astra"), + ] { + let err = Request::new(agent, "hi") + .model(id) + .argv() + .expect_err("a retired model must be refused"); + let crate::Error::RetiredModel { + agent: refused, + model, + retired, + } = &err + else { + panic!("expected RetiredModel, got {err:?}"); + }; + assert_eq!(*refused, agent); + assert_eq!(model, id); + assert_eq!(retired.replacement.as_deref(), Some(replacement)); + assert!( + err.to_string().contains(replacement), + "the display should carry it too: {err}" + ); + } + } + + /// Retirement is per agent and per catalogued id. An id no catalogue + /// knows, and a live one, still pass through verbatim. + #[test] + fn only_a_catalogued_retired_id_is_refused() { + for (agent, id) in [ + (Agent::Claude, "some-model-from-next-year"), + (Agent::Claude, "claude-haiku-4-5"), + (Agent::Claude, "claude-fable-5-1"), + // Retired on Claude, not on Copilot's own catalogue. + (Agent::Copilot, "claude-fable-5"), + (Agent::Codex, "gpt-6-astra"), + ] { + let argv = Request::new(agent, "hi") + .model(id) + .argv() + .unwrap_or_else(|e| panic!("{agent} {id} must not be refused: {e}")); + assert!( + argv.windows(2).any(|w| w[0] == "--model" && w[1] == id), + "{agent} {id} should reach the command line: {argv:?}" + ); + } + } } diff --git a/src/run.rs b/src/run.rs index b2cf052..d116dcb 100644 --- a/src/run.rs +++ b/src/run.rs @@ -570,6 +570,10 @@ pub fn stream(request: &Request, reactor: &nagoya::reactor::Handle) -> Result Date: Thu, 24 Sep 2026 00:30:21 +0700 Subject: [PATCH 4/4] Release 0.5.2 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b521482..73171d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,49 @@ appear in a patch rather than inflating the version toward 1.0 on a crate still shape. **Where that happens the entry says so at the top**, because a version number that under-signals is only acceptable if the changelog over-signals to compensate. +## 0.5.2 + +**Behaviour change in a patch.** A run on a model the catalogue marks retired now fails +with the new `Error::RetiredModel` before anything is spawned, where before it ran. `Model` also +gains a public `retired` field (additive: `Model` is `#[non_exhaustive]`). + +Model catalogues re-checked against the installed CLIs: claude 2.1.280 and codex-cli +0.156.1. Every Claude entry was run once with `--model` and answered under the id it +reports; the Codex list is what `codex debug models` returns. The live Codex suite passed +on 0.156.1 apart from the two tests that need a session fixture. + +### Added + +- **Claude Opus 5.5:** `claude-opus-5-5` and `claude-opus-5-5[1m]`, both 1M. On 2.1.280 + `opus` and `default` resolve to `claude-opus-5-5`, and `opus[1m]` to + `claude-opus-5-5[1m]`. 2.1.267 refused the id as `[claude-code:unrecognized_model]`. +- **Retired models stay in the catalogue.** A model a CLI stops offering is kept with + `Model::retired` set (`Retired { since, replacement, reason }`) instead of being deleted, + because a host that stored the id would otherwise see it vanish from its picker while + its runs kept using it. `Model::is_retired()` flags it, a retired entry is never + `is_default`, and a request naming one fails with `Error::RetiredModel` carrying the model, + the reason and the replacement. Ids the catalogue does not know still pass through. + `discover_models` appends the compiled-in retired entries after the discovered ones. + Retired: `claude-fable-5` (claude 2.1.267, answers as `claude-opus-4-8`; use + `claude-fable-5-1`), and Codex `gpt-5.4` and `gpt-5.4-mini` (codex-cli 0.154.0, no longer + listed; use `gpt-6-astra`). + +### Changed + +- **Codex catalogue** follows what 0.156.1 reports: `gpt-6-sol` and `gpt-6-luna` are new, + between `gpt-6-astra` (still the default) and `gpt-5.6-sol`. `gpt-6-luna` stops at `max`; + `gpt-6-sol` goes to `ultra`. +- **`verified_version`** is claude 2.1.280 and codex 0.156.1, the releases `Probe` now + compares against. `claude --help` on 2.1.280 differs from 2.1.267 only in the wording of + `--bare` and `--safe-mode`; no flag this crate passes changed. +- **Claude catalogue:** `claude-fable-5-1` is added and `claude-fable-5` is retired. Both + `fable` and `best` now resolve to `claude-fable-5-1`, and a run on `claude-fable-5` + produced no output of its own, with the answer coming from `claude-opus-4-8`. + `claude-haiku-4-5-20251001`, the id the `haiku` alias reports, is added beside + `claude-haiku-4-5`, which still answers under its own name and stays live. +- **`claude-opus-5` is 1M natively** since 2.1.267. It reported a 200k window on 2.1.212; + it now reports 1,000,000 without the `[1m]` suffix. + ## 0.5.1 Re-verified against the installed CLIs: claude 2.1.267, codex-cli 0.154.0, GitHub Copilot diff --git a/Cargo.toml b/Cargo.toml index 62db939..d747dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-abstraction" -version = "0.5.1" +version = "0.5.2" edition = "2024" # The floor edition 2024 requires, and where the strictest dependencies (uuid, # getrandom) sit. Derived from the dependency graph rather than compile-tested.