From e6841db50b7a26f45d69c53987a9bd06feb4936b Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 29 May 2026 16:50:37 +0800 Subject: [PATCH 1/5] feat(model): add Claude Opus 4.8 as the default model Opus 4.8 shares 4.7's full capability set (interleaved thinking, context management, 1M context, the Low..Max effort ladder, structured outputs) and the same $5/$25 pricing, so a single most-specific-first MODELS row makes it fully functional: betas, pricing, context window, and the auto-compaction threshold all derive from the table. Make it the default model and the `opus` alias target. The combined model+effort picker lists 4.8 in place of 4.7 (4.7 stays selectable via the table), mirroring how 4.6 was dropped from the roster when 4.7 shipped. oxide-code keeps its highest-non-Max effort heuristic, so 4.8 defaults to xhigh rather than upstream's high default. --- .../oxide-code/src/client/anthropic/betas.rs | 13 ++++ crates/oxide-code/src/config.rs | 5 +- crates/oxide-code/src/model.rs | 60 +++++++++++++------ crates/oxide-code/src/prompt/environment.rs | 1 + crates/oxide-code/src/slash.rs | 2 +- crates/oxide-code/src/slash/model.rs | 32 +++++----- crates/oxide-code/src/slash/picker.rs | 20 +++---- ...er_cancelling_shows_spinner_and_label.snap | 2 +- ...acting_shows_spinner_and_status_label.snap | 2 +- ...med_shows_static_hint_without_spinner.snap | 2 +- ..._with_title_shows_model_title_and_cwd.snap | 2 +- ...idle_without_title_leaves_slot_unused.snap | 2 +- ...w_width_preserves_model_and_run_state.snap | 2 +- ...eaming_shows_spinner_and_status_label.snap | 2 +- ...us__tests__render_tool_running_status.snap | 2 +- ...m_full_layout_still_includes_starters.snap | 2 +- ...ollapsed_drops_box_but_keeps_starters.snap | 2 +- ..._box_environment_starters_and_trailer.snap | 2 +- ...rters_and_truncates_cwd_in_the_middle.snap | 2 +- .../oxide-code/src/tui/components/status.rs | 18 +++--- .../oxide-code/src/tui/components/welcome.rs | 4 +- docs/design/tui/status-line.md | 2 +- docs/design/tui/welcome.md | 4 +- docs/guide/configuration.md | 16 ++--- docs/research/api/anthropic.md | 6 +- docs/research/tui/status-line.md | 12 ++-- 26 files changed, 130 insertions(+), 89 deletions(-) diff --git a/crates/oxide-code/src/client/anthropic/betas.rs b/crates/oxide-code/src/client/anthropic/betas.rs index e9ce667a..79e21ae6 100644 --- a/crates/oxide-code/src/client/anthropic/betas.rs +++ b/crates/oxide-code/src/client/anthropic/betas.rs @@ -209,6 +209,19 @@ mod tests { assert!(with_1m.contains(&CONTEXT_1M_BETA_HEADER)); } + #[test] + fn compute_betas_opus_4_8_matches_opus_4_7_header_order() { + // 4.8 shares 4.7's capability flags, so the fingerprinted header sequence must be + // byte-identical (3P gateways reject mismatches). + assert_eq!( + compute_betas("claude-opus-4-8", &api_key(), true, false), + compute_betas("claude-opus-4-7", &api_key(), true, false), + ); + let with_1m = compute_betas("claude-opus-4-8[1m]", &api_key(), true, false); + assert!(with_1m.contains(&CONTEXT_1M_BETA_HEADER)); + assert!(with_1m.contains(&EFFORT_BETA_HEADER)); + } + #[test] fn compute_betas_structured_outputs_gated_by_model_capability() { assert_eq!( diff --git a/crates/oxide-code/src/config.rs b/crates/oxide-code/src/config.rs index 627b10bb..df31713c 100644 --- a/crates/oxide-code/src/config.rs +++ b/crates/oxide-code/src/config.rs @@ -17,7 +17,7 @@ use crate::tui::theme::{self, Theme}; use crate::util::env; use crate::util::path::expand_user; -const DEFAULT_MODEL: &str = "claude-opus-4-7[1m]"; +const DEFAULT_MODEL: &str = "claude-opus-4-8[1m]"; const DEFAULT_BASE_URL: &str = "https://api.anthropic.com"; const AUTO_COMPACTION_OUTPUT_RESERVE_CAP: u32 = 20_000; const AUTO_COMPACTION_BUFFER_TOKENS: u32 = 13_000; @@ -879,7 +879,7 @@ mod tests { #[tokio::test] async fn load_defaults_apply_when_no_config_and_no_env() { - // Opus 4.7 supports `xhigh`; `effort` / `max_tokens` derive from that ceiling. + // Opus 4.8 supports `xhigh`; `effort` / `max_tokens` derive from that ceiling. let dir = tempfile::tempdir().unwrap(); let config = temp_env::async_with_vars(env_vars(vec![xdg(&dir)]), Config::load()) .await @@ -1507,6 +1507,7 @@ mod tests { #[tokio::test] async fn load_effort_default_follows_model_ceiling() { for (model, expected) in [ + ("claude-opus-4-8", Some(Effort::Xhigh)), ("claude-opus-4-7", Some(Effort::Xhigh)), ("claude-opus-4-6", Some(Effort::High)), ("claude-sonnet-4-6", Some(Effort::High)), diff --git a/crates/oxide-code/src/model.rs b/crates/oxide-code/src/model.rs index f17ef0a1..7abd6a9c 100644 --- a/crates/oxide-code/src/model.rs +++ b/crates/oxide-code/src/model.rs @@ -51,6 +51,25 @@ pub(crate) struct Capabilities { /// Most-specific substring first. pub(crate) const MODELS: &[ModelInfo] = &[ + ModelInfo { + id_substr: "claude-opus-4-8", + display_name: "Claude Opus 4.8", + cutoff: Some("January 2026"), + capabilities: Capabilities { + interleaved_thinking: true, + context_management: true, + context_1m: true, + supported_efforts: &[ + Effort::Low, + Effort::Medium, + Effort::High, + Effort::Xhigh, + Effort::Max, + ], + structured_outputs: true, + }, + cost_rates: Some(OPUS_4_5_PLUS_RATES), + }, ModelInfo { id_substr: "claude-opus-4-7", display_name: "Claude Opus 4.7", @@ -279,10 +298,10 @@ mod tests { #[test] fn capability_flags_match_upstream_substring_predicates() { - // Locks substring-derived flags to upstream's `modelSupports*` predicates. Opus 4.7 - // postdates the predicate set and is skipped. + // Locks substring-derived flags to upstream's `modelSupports*` predicates. Opus 4.7 and + // 4.8 postdate the predicate set and are skipped. for info in MODELS { - if info.id_substr == "claude-opus-4-7" { + if matches!(info.id_substr, "claude-opus-4-7" | "claude-opus-4-8") { continue; } let m = info.id_substr; @@ -306,15 +325,17 @@ mod tests { } #[test] - fn opus_4_7_uniquely_supports_xhigh() { - // Upstream predates 4.7; pin so a future "alignment" edit doesn't strip our caps. - let caps = lookup("claude-opus-4-7").unwrap().capabilities; - assert!(caps.interleaved_thinking); - assert!(caps.context_management); - assert!(caps.context_1m); - assert!(caps.accepts_effort(Effort::Xhigh)); - assert!(caps.accepts_effort(Effort::Max)); - assert!(caps.structured_outputs); + fn opus_4_7_and_4_8_support_the_full_effort_ladder() { + // Upstream predates 4.7 / 4.8; pin so a future "alignment" edit doesn't strip our caps. + for id in ["claude-opus-4-7", "claude-opus-4-8"] { + let caps = lookup(id).unwrap().capabilities; + assert!(caps.interleaved_thinking, "{id}"); + assert!(caps.context_management, "{id}"); + assert!(caps.context_1m, "{id}"); + assert!(caps.accepts_effort(Effort::Xhigh), "{id}"); + assert!(caps.accepts_effort(Effort::Max), "{id}"); + assert!(caps.structured_outputs, "{id}"); + } for other in [ "claude-opus-4-6", @@ -329,14 +350,14 @@ mod tests { .unwrap() .capabilities .accepts_effort(Effort::Xhigh), - "{other} must not accept Xhigh — it 400s on non-4.7", + "{other} must not accept Xhigh — it 400s on pre-4.7 models", ); } } #[test] fn effort_max_is_opus_only() { - for supported in ["claude-opus-4-7", "claude-opus-4-6"] { + for supported in ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"] { assert!( lookup(supported) .unwrap() @@ -378,6 +399,7 @@ mod tests { #[test] fn structured_outputs_flag_tracks_upstream_allowlist() { for supported in [ + "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", @@ -456,9 +478,11 @@ mod tests { #[test] fn default_effort_picks_highest_supported_tier_when_user_has_no_pick() { - // Opus 4.7: full ladder → xhigh. - let opus_4_7 = lookup("claude-opus-4-7").unwrap().capabilities; - assert_eq!(opus_4_7.default_effort(), Some(Effort::Xhigh)); + // Opus 4.7 / 4.8: full ladder → xhigh. + for id in ["claude-opus-4-7", "claude-opus-4-8"] { + let caps = lookup(id).unwrap().capabilities; + assert_eq!(caps.default_effort(), Some(Effort::Xhigh), "{id}"); + } // Opus 4.6 / Sonnet 4.6: effort but no xhigh → high. for id in ["claude-opus-4-6", "claude-sonnet-4-6"] { @@ -613,6 +637,7 @@ mod tests { #[test] fn display_name_known_plain_id_renders_row_label() { for (id, expected) in [ + ("claude-opus-4-8", "Claude Opus 4.8"), ("claude-opus-4-7", "Claude Opus 4.7"), ("claude-opus-4-6", "Claude Opus 4.6"), ("claude-sonnet-4-6", "Claude Sonnet 4.6"), @@ -649,6 +674,7 @@ mod tests { #[test] fn short_display_name_strips_claude_family_prefix() { for (id, expected) in [ + ("claude-opus-4-8", "Opus 4.8"), ("claude-opus-4-7", "Opus 4.7"), ("claude-sonnet-4-6", "Sonnet 4.6"), ("claude-haiku-4-5", "Haiku 4.5"), diff --git a/crates/oxide-code/src/prompt/environment.rs b/crates/oxide-code/src/prompt/environment.rs index 8ec67576..1a850be2 100644 --- a/crates/oxide-code/src/prompt/environment.rs +++ b/crates/oxide-code/src/prompt/environment.rs @@ -253,6 +253,7 @@ mod tests { #[test] fn knowledge_cutoff_known_models() { for (id, expected) in [ + ("claude-opus-4-8", "January 2026"), ("claude-opus-4-7", "January 2026"), ("claude-sonnet-4-6", "August 2025"), ("claude-opus-4-6", "May 2025"), diff --git a/crates/oxide-code/src/slash.rs b/crates/oxide-code/src/slash.rs index aaf0f075..2b10f462 100644 --- a/crates/oxide-code/src/slash.rs +++ b/crates/oxide-code/src/slash.rs @@ -135,7 +135,7 @@ pub(crate) fn test_session_info() -> LiveSessionInfo { auth_label: "API key", base_url: "https://api.test.invalid".to_owned(), extra_ca_certs: None, - model_id: "claude-opus-4-7".to_owned(), + model_id: "claude-opus-4-8".to_owned(), effort: Some(Effort::High), max_tokens: 32_000, max_tool_rounds: None, diff --git a/crates/oxide-code/src/slash/model.rs b/crates/oxide-code/src/slash/model.rs index 2f65415f..c642724a 100644 --- a/crates/oxide-code/src/slash/model.rs +++ b/crates/oxide-code/src/slash/model.rs @@ -15,7 +15,7 @@ use crate::model::{MODELS, ResolvedModelId, display_name, lookup}; const TAG_1M: &str = "[1m]"; const ALIASES: &[(&str, &str)] = &[ - ("opus", "claude-opus-4-7"), + ("opus", "claude-opus-4-8"), ("sonnet", "claude-sonnet-4-6"), ("haiku", "claude-haiku-4-5"), ]; @@ -79,7 +79,7 @@ fn resolve_model_arg(arg: &str) -> Result { }; if base_arg.is_empty() { return Err(format!( - "`{TAG_1M}` is a tag, not a model. Try `/model opus{TAG_1M}` or `/model claude-opus-4-7{TAG_1M}`.", + "`{TAG_1M}` is a tag, not a model. Try `/model opus{TAG_1M}` or `/model claude-opus-4-8{TAG_1M}`.", )); } let base_id = resolve_base(base_arg)?; @@ -238,19 +238,19 @@ mod tests { .into_iter() .map(|(v, _)| v) .collect(); - assert_eq!(got, vec!["claude-opus-4-7", "claude-opus-4-7[1m]"]); + assert_eq!(got, vec!["claude-opus-4-8", "claude-opus-4-8[1m]"]); } #[test] fn complete_arg_appends_1m_context_suffix_only_for_1m_variants() { - let rows = arg_rows("claude-opus-4-7"); + let rows = arg_rows("claude-opus-4-8"); let one_m = rows .iter() - .find(|(v, _)| v == "claude-opus-4-7[1m]") + .find(|(v, _)| v == "claude-opus-4-8[1m]") .expect("1M variant present"); let plain = rows .iter() - .find(|(v, _)| v == "claude-opus-4-7") + .find(|(v, _)| v == "claude-opus-4-8") .expect("plain variant present"); assert!( one_m.1.contains("1M context"), @@ -296,8 +296,8 @@ mod tests { #[test] fn execute_with_alias_resolves_to_canonical_id() { for (alias, expected) in [ - ("opus", "claude-opus-4-7"), - ("opus[1m]", "claude-opus-4-7[1m]"), + ("opus", "claude-opus-4-8"), + ("opus[1m]", "claude-opus-4-8[1m]"), ("sonnet", "claude-sonnet-4-6"), ("sonnet[1m]", "claude-sonnet-4-6[1m]"), ("haiku", "claude-haiku-4-5"), @@ -391,7 +391,7 @@ mod tests { ); } assert!( - msg.contains("Claude Opus 4.7 (1M context)"), + msg.contains("Claude Opus 4.8 (1M context)"), "1M variant renders the (1M context) suffix: {msg}", ); assert_eq!(chat.entry_count(), 0); @@ -427,8 +427,8 @@ mod tests { fn execute_ambiguous_listing_falls_back_to_full_curated_set_when_filter_empty() { let (_, outcome) = run_execute("claude-opus"); let msg = outcome.expect_err("ambiguous arg must error"); - // `claude-opus` matches the listed Opus 4.7 entries, so the listing surfaces those. - for id in ["claude-opus-4-7", "claude-opus-4-7[1m]"] { + // `claude-opus` matches the listed Opus 4.8 entries, so the listing surfaces those. + for id in ["claude-opus-4-8", "claude-opus-4-8[1m]"] { assert!(msg.contains(id), "{id} should be listed: {msg}"); } // Older non-listed Opus rows must not appear in the curated listing. @@ -447,7 +447,7 @@ mod tests { resolve_model_arg("opus") .as_ref() .map(ResolvedModelId::as_str), - Ok("claude-opus-4-7") + Ok("claude-opus-4-8") ); } @@ -489,7 +489,7 @@ mod tests { resolve_model_arg("OPUS") .as_ref() .map(ResolvedModelId::as_str), - Ok("claude-opus-4-7") + Ok("claude-opus-4-8") ); assert_eq!( resolve_model_arg("Claude-Opus-4-7") @@ -501,7 +501,7 @@ mod tests { resolve_model_arg("OPUS[1M]") .as_ref() .map(ResolvedModelId::as_str), - Ok("claude-opus-4-7[1m]"), + Ok("claude-opus-4-8[1m]"), ); } @@ -543,11 +543,11 @@ mod tests { fn listed_models_matching_surfaces_1m_variants_alongside_base() { let out = listed_models_matching("claude-opus"); assert!( - out.contains("- `claude-opus-4-7` — Claude Opus 4.7"), + out.contains("- `claude-opus-4-8` — Claude Opus 4.8"), "{out}" ); assert!( - out.contains("- `claude-opus-4-7[1m]` — Claude Opus 4.7 (1M context)"), + out.contains("- `claude-opus-4-8[1m]` — Claude Opus 4.8 (1M context)"), "{out}", ); } diff --git a/crates/oxide-code/src/slash/picker.rs b/crates/oxide-code/src/slash/picker.rs index 677595fe..79077827 100644 --- a/crates/oxide-code/src/slash/picker.rs +++ b/crates/oxide-code/src/slash/picker.rs @@ -22,8 +22,8 @@ use crate::tui::theme::Theme; /// Curated roster shown in the picker AND in `/model`'s typed-arg autocomplete; `/model ` /// still resolves against the full `MODELS` table. pub(super) const LISTED_MODELS: &[&str] = &[ - "claude-opus-4-7", - "claude-opus-4-7[1m]", + "claude-opus-4-8", + "claude-opus-4-8[1m]", "claude-sonnet-4-6", "claude-sonnet-4-6[1m]", "claude-haiku-4-5", @@ -329,10 +329,10 @@ mod tests { #[test] fn new_positions_cursor_on_active_model() { - // `test_session_info` ships claude-opus-4-7 active. + // `test_session_info` ships claude-opus-4-8 active. let p = picker(); let row = p.list.selected().expect("active row"); - assert_eq!(row.id, "claude-opus-4-7"); + assert_eq!(row.id, "claude-opus-4-8"); assert!(row.is_active); } @@ -366,7 +366,7 @@ mod tests { #[test] fn right_arrow_cycles_effort_within_supported_levels() { - // Opus 4.7 supports the full ladder. Pressing Right walks + // Opus 4.8 supports the full ladder. Pressing Right walks // through it; Left walks back. let mut p = picker(); let initial = p.effort; @@ -396,7 +396,7 @@ mod tests { // — pin it independently. Cycle Left until the effort returns to the initial pick. let mut p = picker(); p.handle_key(&key(KeyCode::Right)); // arm the axis with a known starting tier - let initial = p.effort.expect("Opus 4.7 has an effort axis"); + let initial = p.effort.expect("Opus 4.8 has an effort axis"); for _ in 0..16 { p.handle_key(&key(KeyCode::Left)); if p.effort == Some(initial) { @@ -439,7 +439,7 @@ mod tests { ModalKey::Submitted(ModalAction::User(UserAction::SwapConfig { model, effort })) => { assert_eq!( model.map(ResolvedModelId::into_inner).as_deref(), - Some("claude-opus-4-7[1m]"), + Some("claude-opus-4-8[1m]"), ); assert!( effort.is_none(), @@ -452,7 +452,7 @@ mod tests { #[test] fn enter_after_effort_change_emits_swap_with_effort_only() { - // Opus 4.7 active + High; cycle effort Forward once → Xhigh. + // Opus 4.8 active + High; cycle effort Forward once → Xhigh. let mut p = picker(); p.handle_key(&key(KeyCode::Right)); let outcome = p.handle_key(&key(KeyCode::Enter)); @@ -491,10 +491,10 @@ mod tests { #[test] fn render_runs_at_typical_widths_without_panicking() { let theme = Theme::default(); - // Two cursor positions: an effort-tier model (Opus 4.7) so the effort row renders, and + // Two cursor positions: an effort-tier model (Opus 4.8) so the effort row renders, and // a no-tier model (Haiku 4.5) so the hide branch executes. for setup in [ - None, // Opus 4.7 — has effort tier + None, // Opus 4.8 — has effort tier Some(KeyCode::Char('5')), // Haiku 4.5 — no effort tier ] { let mut p = picker(); diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_cancelling_shows_spinner_and_label.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_cancelling_shows_spinner_and_label.snap index e2dfaa7c..ad60468c 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_cancelling_shows_spinner_and_label.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_cancelling_shows_spinner_and_label.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ ⣷ Cancelling " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ ⣷ Cancelling " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_compacting_shows_spinner_and_status_label.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_compacting_shows_spinner_and_status_label.snap index 5df03297..b082e1e7 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_compacting_shows_spinner_and_status_label.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_compacting_shows_spinner_and_status_label.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ ⣷ Compacting · Esc to interrupt " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ ⣷ Compacting · Esc to interrupt " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_exit_armed_shows_static_hint_without_spinner.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_exit_armed_shows_static_hint_without_spinner.snap index 9585ff7d..18cf0afb 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_exit_armed_shows_static_hint_without_spinner.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_exit_armed_shows_static_hint_without_spinner.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ Press Ctrl+C again to exit " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ Press Ctrl+C again to exit " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_with_title_shows_model_title_and_cwd.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_with_title_shows_model_title_and_cwd.snap index a29427c9..0e179fcb 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_with_title_shows_model_title_and_cwd.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_with_title_shows_model_title_and_cwd.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ Ready │ Fix login flow " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ Ready │ Fix login flow " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_without_title_leaves_slot_unused.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_without_title_leaves_slot_unused.snap index 9f86bb35..585b5735 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_without_title_leaves_slot_unused.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_idle_without_title_leaves_slot_unused.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ Ready " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ Ready " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_narrow_width_preserves_model_and_run_state.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_narrow_width_preserves_model_and_run_state.snap index 1af8661e..e28cc371 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_narrow_width_preserves_model_and_run_state.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_narrow_width_preserves_model_and_run_state.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 40)" --- -" Opus 4.7 (xhigh) │ Ready " +" Opus 4.8 (xhigh) │ Ready " "────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_streaming_shows_spinner_and_status_label.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_streaming_shows_spinner_and_status_label.snap index a522af7b..a14e2325 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_streaming_shows_spinner_and_status_label.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_streaming_shows_spinner_and_status_label.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ ⣷ Streaming · Esc to interrupt " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ ⣷ Streaming · Esc to interrupt " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_tool_running_status.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_tool_running_status.snap index 5e260f71..c5afa39f 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_tool_running_status.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__status__tests__render_tool_running_status.snap @@ -2,5 +2,5 @@ source: crates/oxide-code/src/tui/components/status.rs expression: "render_status(&mut bar, 80)" --- -" ~/projects/demo │ main │ Opus 4.7 (xhigh) │ ⣷ Running bash · Esc to interrupt " +" ~/projects/demo │ main │ Opus 4.8 (xhigh) │ ⣷ Running bash · Esc to interrupt " "────────────────────────────────────────────────────────────────────────────────" diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_60_col_minimum_full_layout_still_includes_starters.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_60_col_minimum_full_layout_still_includes_starters.snap index 0635473f..765060fa 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_60_col_minimum_full_layout_still_includes_starters.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_60_col_minimum_full_layout_still_includes_starters.snap @@ -5,7 +5,7 @@ expression: "render(60, 14, &snap)" " " " ━━━━ oxide-code v0.1.0 ━━━━ " " " -" Claude Opus 4.7 · xhigh effort · OAuth " +" Claude Opus 4.8 · xhigh effort · OAuth " " ~/github/oxide-code " " " " Try one of: " diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_collapsed_drops_box_but_keeps_starters.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_collapsed_drops_box_but_keeps_starters.snap index 541c1e4f..85df338b 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_collapsed_drops_box_but_keeps_starters.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_collapsed_drops_box_but_keeps_starters.snap @@ -5,7 +5,7 @@ expression: "render(50, 14, &snap)" " " " oxide-code v0.1.0 " " " -" Claude Opus 4.7 · xhigh effort " +" Claude Opus 4.8 · xhigh effort " " ~/github/oxide-code " " " " Try one of: " diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_full_width_renders_box_environment_starters_and_trailer.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_full_width_renders_box_environment_starters_and_trailer.snap index 0c65a5e8..f46b26eb 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_full_width_renders_box_environment_starters_and_trailer.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_full_width_renders_box_environment_starters_and_trailer.snap @@ -5,7 +5,7 @@ expression: "render(80, 14, &snap)" " " " ━━━━ oxide-code v0.1.0 ━━━━ " " " -" Claude Opus 4.7 · xhigh effort · OAuth " +" Claude Opus 4.8 · xhigh effort · OAuth " " ~/github/oxide-code " " " " Try one of: " diff --git a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_narrow_drops_starters_and_truncates_cwd_in_the_middle.snap b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_narrow_drops_starters_and_truncates_cwd_in_the_middle.snap index 29a64cf9..a61a3aa3 100644 --- a/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_narrow_drops_starters_and_truncates_cwd_in_the_middle.snap +++ b/crates/oxide-code/src/tui/components/snapshots/ox__tui__components__welcome__tests__paint_narrow_drops_starters_and_truncates_cwd_in_the_middle.snap @@ -5,7 +5,7 @@ expression: "render(30, 8, &snap)" " " " oxide-code v0.1.0 " " " -"Claude Opus 4.7 · xhigh " +"Claude Opus 4.8 · xhigh " "~/very/long/w...ath/oxide-code" " " " " diff --git a/crates/oxide-code/src/tui/components/status.rs b/crates/oxide-code/src/tui/components/status.rs index 710ee7cb..13233029 100644 --- a/crates/oxide-code/src/tui/components/status.rs +++ b/crates/oxide-code/src/tui/components/status.rs @@ -363,7 +363,7 @@ mod tests { StatusBar::new( &Theme::default(), vec![StatusLineSegment::PullRequest, StatusLineSegment::RunState], - "Opus 4.7".into(), + "Opus 4.8".into(), None, String::new(), None, @@ -407,11 +407,11 @@ mod tests { #[test] fn set_model_replaces_displayed_model_label() { let mut bar = test_bar(); - bar.set_model("Opus 4.7".to_owned()); - assert_eq!(bar.model(), "Opus 4.7"); + bar.set_model("Opus 4.8".to_owned()); + assert_eq!(bar.model(), "Opus 4.8"); let output = render_top_row(&mut bar, 80); assert!( - output.contains("Opus 4.7"), + output.contains("Opus 4.8"), "new label must reach the rendered bar: {output:?}", ); assert!( @@ -727,7 +727,7 @@ mod tests { let mut bar = StatusBar::new( &Theme::default(), StatusLineSegment::DEFAULT.to_vec(), - "Opus 4.7".into(), + "Opus 4.8".into(), Some(Effort::Xhigh), cwd.into(), None, @@ -814,7 +814,7 @@ mod tests { StatusLineSegment::Model, StatusLineSegment::CurrentDir, ], - "Opus 4.7".into(), + "Opus 4.8".into(), Some(Effort::Xhigh), "~/projects/demo".into(), None, @@ -822,7 +822,7 @@ mod tests { ); let output = render_top_row(&mut bar, 120); let state_at = output.find("Ready").unwrap(); - let model_at = output.find("Opus 4.7").unwrap(); + let model_at = output.find("Opus 4.8").unwrap(); let cwd_at = output.find("~/projects/demo").unwrap(); assert!(state_at < model_at, "run state should lead: {output:?}"); assert!(model_at < cwd_at, "cwd should follow model: {output:?}"); @@ -842,14 +842,14 @@ mod tests { StatusLineSegment::ModelWithEffort, StatusLineSegment::RunState, ], - "Opus 4.7".into(), + "Opus 4.8".into(), Some(Effort::Xhigh), "~/projects/demo".into(), None, Some("feat/status-line".to_owned()), ); let output = render_top_row(&mut bar, 120); - assert!(output.contains("~/projects/demo │ feat/status-line │ Opus 4.7 (xhigh) │ Ready")); + assert!(output.contains("~/projects/demo │ feat/status-line │ Opus 4.8 (xhigh) │ Ready")); } #[test] diff --git a/crates/oxide-code/src/tui/components/welcome.rs b/crates/oxide-code/src/tui/components/welcome.rs index e1f2d91c..b0bf4adf 100644 --- a/crates/oxide-code/src/tui/components/welcome.rs +++ b/crates/oxide-code/src/tui/components/welcome.rs @@ -353,7 +353,7 @@ mod tests { auth_label: "OAuth", base_url: "https://api.test.invalid".to_owned(), extra_ca_certs: None, - model_id: "claude-opus-4-7".to_owned(), + model_id: "claude-opus-4-8".to_owned(), effort: Some(Effort::Xhigh), max_tokens: 64_000, max_tool_rounds: None, @@ -392,7 +392,7 @@ mod tests { let info = fixture(); let snap = snap_for(&info); assert_eq!(snap.version, "0.1.0"); - assert_eq!(snap.model_label, "Claude Opus 4.7"); + assert_eq!(snap.model_label, "Claude Opus 4.8"); assert_eq!(snap.effort_label, "xhigh"); assert_eq!(snap.auth_label, "OAuth"); assert_eq!(snap.cwd, "~/github/oxide-code"); diff --git a/docs/design/tui/status-line.md b/docs/design/tui/status-line.md index 819bb6d7..7023703c 100644 --- a/docs/design/tui/status-line.md +++ b/docs/design/tui/status-line.md @@ -47,7 +47,7 @@ Segment render rules: - Use active theme styles; color customization belongs in theme overrides. - Join only rendered segments, so omitted segments do not leave extra separators. - When the row is too narrow, omit lower-utility segments before truncating the last remaining segment. Run state and model have the highest utility. -- The `model` and `model-with-effort` segments use a width-tightened label: the `Claude` family prefix is dropped and `(1M context)` becomes `[1M]` (e.g., `Opus 4.7 [1M] (xhigh)`). Other surfaces (welcome screen, prompt environment, error blocks) keep the full `Claude Opus 4.7 (1M context)` label. +- The `model` and `model-with-effort` segments use a width-tightened label: the `Claude` family prefix is dropped and `(1M context)` becomes `[1M]` (e.g., `Opus 4.8 [1M] (xhigh)`). Other surfaces (welcome screen, prompt environment, error blocks) keep the full `Claude Opus 4.8 (1M context)` label. ## Usage Data diff --git a/docs/design/tui/welcome.md b/docs/design/tui/welcome.md index 2aa0fc73..209cd2b5 100644 --- a/docs/design/tui/welcome.md +++ b/docs/design/tui/welcome.md @@ -21,12 +21,12 @@ The minimal `Welcome to ox / Ask anything to begin.` banner gets users to the pr ## Layout -At 80 cols with the test fixture (`claude-opus-4-7` plain, `xhigh` effort, OAuth), the render is: +At 80 cols with the test fixture (`claude-opus-4-8` plain, `xhigh` effort, OAuth), the render is: ```text ━━━━ oxide-code v0.1.0 ━━━━ - Claude Opus 4.7 · xhigh effort · OAuth + Claude Opus 4.8 · xhigh effort · OAuth ~/github/oxide-code Try one of: diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 47e96bc0..c255d563 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -45,7 +45,7 @@ status_line = [ | `api_key` | string | - | Anthropic API key (user config only) | | `base_url` | string | `https://api.anthropic.com` | API base URL (user config only) | | `extra_ca_certs` | string | - | PEM bundle appended to the trust store (user config only) | -| `model` | string | `claude-opus-4-7[1m]` | Model to use | +| `model` | string | `claude-opus-4-8[1m]` | Model to use | | `effort` | string | per-model (see below) | Intelligence-vs-latency tier | | `max_tokens` | integer | effort-derived (see below) | Max tokens per response | | `max_tool_rounds` | integer | unset (unbounded) | Per-turn safety cap on tool rounds | @@ -59,15 +59,15 @@ Per-model defaults when `effort` is unset: | Model | Default | | --------------- | ------- | -| Opus 4.7 | `xhigh` | +| Opus 4.8 / 4.7 | `xhigh` | | Opus 4.6 | `high` | | Sonnet 4.6 | `high` | | Everything else | (unset) | -Tier guide (from the [Opus 4.7 migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide)): +Tier guide (from the [Opus 4.8 migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide)): - `max`: Deepest reasoning, Opus-only, with diminishing returns on some tasks. -- `xhigh`: Recommended default for coding and agentic work on Opus 4.7. +- `xhigh`: Recommended default for coding and agentic work on Opus 4.7 / 4.8. - `high`: Balanced, minimum recommended for intelligence-sensitive tasks. - `medium`: Cost-sensitive workloads. - `low`: Scoped, latency-sensitive tasks. @@ -120,7 +120,7 @@ Append `[1m]` to `model` to opt into the 1M-token context window on models that ```toml [client] -model = "claude-opus-4-7[1m]" +model = "claude-opus-4-8[1m]" ``` 1M access depends on your subscription or gateway, so you have to opt in explicitly. The tag is silently ignored on models without 1M support (e.g. Haiku). @@ -133,7 +133,7 @@ model = "claude-opus-4-7[1m]" | `show_welcome` | boolean | `true` | Paint the welcome splash on an empty chat | | `status_line` | array | see below | Ordered status-line segments | -On Opus 4.7, `show_thinking = true` opts the request into `thinking.display = "summarized"` so the API streams reasoning text. Otherwise the 4.7 default of `"omitted"` applies and the UI sees nothing until the final answer arrives. +On Opus 4.7 / 4.8, `show_thinking = true` opts the request into `thinking.display = "summarized"` so the API streams reasoning text. Otherwise the `"omitted"` default applies and the UI sees nothing until the final answer arrives. `show_welcome = false` blanks the chat region until you submit a prompt, which is useful when piping or screen-recording. @@ -144,7 +144,7 @@ On Opus 4.7, `show_thinking = true` opts the request into `thinking.display = "s | `current-dir` | Tildified working directory | At startup | | `git-branch` | Current branch (omitted on detached HEAD) | Every 5 s | | `pull-request` | Open PR as clickable `#86` when OSC 8 works | Every 60 s | -| `model` | Compact model label (e.g., `Opus 4.7`) | On `/model` | +| `model` | Compact model label (e.g., `Opus 4.8`) | On `/model` | | `model-with-effort` | Model label plus the effort tier in parens | On `/model` | | `context-used` | `Ctx: 50% (100k/200k)` after the first turn | Per turn | | `session-cost` | `Sess: $0.4321` running USD estimate | Per turn | @@ -196,7 +196,7 @@ Environment variables override all config file values. | -------------------------------------- | ------------------------------------------ | --------------------------- | ---------------------------- | | `ANTHROPIC_API_KEY` | `client.api_key` | - | Anthropic API key | | `ANTHROPIC_BASE_URL` | `client.base_url` | `https://api.anthropic.com` | API base URL | -| `ANTHROPIC_MODEL` | `client.model` | `claude-opus-4-7[1m]` | Model to use | +| `ANTHROPIC_MODEL` | `client.model` | `claude-opus-4-8[1m]` | Model to use | | `ANTHROPIC_EFFORT` | `client.effort` | per-model | Intelligence-vs-latency tier | | `ANTHROPIC_MAX_TOKENS` | `client.max_tokens` | effort-derived | Max tokens per response | | `OX_EXTRA_CA_CERTS` | `client.extra_ca_certs` | - | Path to a PEM trust bundle | diff --git a/docs/research/api/anthropic.md b/docs/research/api/anthropic.md index ee50c96e..7ca29f2d 100644 --- a/docs/research/api/anthropic.md +++ b/docs/research/api/anthropic.md @@ -85,7 +85,7 @@ Key rules: - **Haiku + `interleaved-thinking`**: Third-party gateways reject it, while first-party accepts. - **Haiku one-shots** (title generation, compaction classifier): Strip agentic markers entirely. `claude-code-20250219` is re-added only when the call is agentic. - **`prompt-caching-scope` ships unconditionally.** The header alone is a server-side no-op without a matching `cache_control.scope` field, but 3P gateways fingerprint its absence. oxide-code therefore emits the beta on every agentic request and gates only the body-side `cache_control.scope: "global"` on `is_first_party_base_url()` (see [Prompt Caching Scope](#prompt-caching-scope) for why). -- **`context-1m` is user opt-in via `[1m]`.** Appending `[1m]` to the model string (e.g., `claude-opus-4-7[1m]`) adds the 1M beta and strips the tag before the request hits the wire. Family-based auto-enable would 400 on subscriptions or gateways that do not carry 1M access. Convention matches Claude Code. +- **`context-1m` is user opt-in via `[1m]`.** Appending `[1m]` to the model string (e.g., `claude-opus-4-8[1m]`) adds the 1M beta and strips the tag before the request hits the wire. Family-based auto-enable would 400 on subscriptions or gateways that do not carry 1M access. Convention matches Claude Code. - **`effort` is Opus 4.6+ and Sonnet 4.6+ only.** Opus 4.5 and older, Sonnet 4.5 and older, and all Haiku variants reject it per upstream's `modelSupportsEffort`. Per-model support is encoded in `Capabilities::supported_efforts`; `accepts_effort`, `clamp_effort`, and `default_effort` keep user picks and defaults inside that set. - **`effort` and `context-management` betas need a body field.** Sending the header alone is a silent no-op: the request runs at the server default. See [Agentic Request Body Fields](#agentic-request-body-fields) for the matching `output_config.effort` and `context_management.edits` shapes. oxide-code pairs each capability with both its beta and its body field so the two stay in sync. - **`structured-outputs` is per-version and caller-opt-in.** The upstream allowlist is Opus 4.1 / 4.5 / 4.6+, Sonnet 4.5 / 4.6+, Haiku 4.5. The beta ships only when a caller supplies an `output_config.format` (today: the AI-title generator). The body field and header are paired on the same capability flag. A schema passed to an unsupported model silently falls back to free-form text, mirroring the `[1m]` × `context_1m` silent-strip pattern. @@ -237,8 +237,8 @@ GA as of Opus 4.6. Controls the intelligence-vs-latency tier of agentic turns vi ``` - **The `effort-2025-11-24` beta header is necessary but not sufficient.** oxide-code used to send the header without the body field. The header became a no-op and the model ran at an undefined default. -- **Per-model ceiling.** `max` is Opus-only, and Sonnet 4.6 400s on it. `xhigh` is Opus 4.7-only. `Capabilities::supported_efforts` encodes the allowed set, and `Capabilities::clamp_effort` clamps a user pick down to the highest supported level at or below it. -- **Per-model default.** Claude Code 2.1.119 sends `xhigh` on Opus 4.7, `high` on Opus 4.6 and Sonnet 4.6, omits the field entirely on earlier models. oxide-code mirrors this via `Capabilities::default_effort`. +- **Per-model ceiling.** `max` is Opus-only, and Sonnet 4.6 400s on it. `xhigh` is Opus 4.7 / 4.8-only. `Capabilities::supported_efforts` encodes the allowed set, and `Capabilities::clamp_effort` clamps a user pick down to the highest supported level at or below it. +- **Per-model default.** `Capabilities::default_effort` picks the highest non-`max` tier: `xhigh` on Opus 4.7 / 4.8, `high` on Opus 4.6 and Sonnet 4.6, omitted on models without effort. This tracks Claude Code for 4.6 / 4.7; oxide-code keeps `xhigh` as the 4.8 default even though upstream lowered the stock 4.8 default to `high`. - **`max_tokens` should scale with effort.** Claude Code uses 64 K on Opus 4.7 at `xhigh`, 32 K on Sonnet 4.6 at `high`. oxide-code's `default_max_tokens(effort)` matches the upper tiers and uses 16 K otherwise when the user hasn't set `ANTHROPIC_MAX_TOKENS` explicitly. ### `context_management.edits` diff --git a/docs/research/tui/status-line.md b/docs/research/tui/status-line.md index 066f6358..4ea2ba05 100644 --- a/docs/research/tui/status-line.md +++ b/docs/research/tui/status-line.md @@ -39,12 +39,12 @@ opencode keeps usage in the prompt footer rather than a fully configurable statu Anthropic's pricing page lists first-party Claude API prices in USD per million tokens and separate prompt-cache rates for 5-minute writes, 1-hour writes, cache reads, and output tokens. Checked on 2026-05-14, the relevant rows for oxide-code's model table are: -| Family | Input | 5m cache write | 1h cache write | Cache read | Output | -| -------------------- | ----- | -------------- | -------------- | ---------- | ------ | -| Opus 4.7 / 4.6 / 4.5 | $5 | $6.25 | $10 | $0.50 | $25 | -| Opus 4.1 | $15 | $18.75 | $30 | $1.50 | $75 | -| Sonnet 4.x | $3 | $3.75 | $6 | $0.30 | $15 | -| Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 | +| Family | Input | 5m cache write | 1h cache write | Cache read | Output | +| -------------------------- | ----- | -------------- | -------------- | ---------- | ------ | +| Opus 4.8 / 4.7 / 4.6 / 4.5 | $5 | $6.25 | $10 | $0.50 | $25 | +| Opus 4.1 | $15 | $18.75 | $30 | $1.50 | $75 | +| Sonnet 4.x | $3 | $3.75 | $6 | $0.30 | $15 | +| Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 | Cost display should stay best-effort because account discounts, marketplace billing, data residency, fast mode, and server-side tool pricing can change the final bill. From 485f5b48f9e88447e4f25d097d347850113a2131 Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 29 May 2026 16:56:41 +0800 Subject: [PATCH 2/5] refactor(model): drop retired Opus 4.1 Opus 4.1 is no longer a supported model. Remove its MODELS row, the OPUS_4_1_RATES pricing constant, and the tests that pinned its older $15/$75 pricing and display labels. `lookup` now treats `claude-opus-4-1` as a retired family that resolves to nothing, asserted alongside the other absent ids. Collapse the Opus 4.1 references in the API and status-line research docs into the 4.5 tier, which carries the same beta profile and is the lowest Opus the table still models. --- crates/oxide-code/src/model.rs | 33 ++------------------------ crates/oxide-code/src/model/pricing.rs | 8 ------- crates/oxide-code/src/slash/model.rs | 1 - docs/research/api/anthropic.md | 22 ++++++++--------- docs/research/tui/status-line.md | 1 - 5 files changed, 13 insertions(+), 52 deletions(-) diff --git a/crates/oxide-code/src/model.rs b/crates/oxide-code/src/model.rs index 7abd6a9c..d1d9aa40 100644 --- a/crates/oxide-code/src/model.rs +++ b/crates/oxide-code/src/model.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; use crate::config::Effort; pub(crate) use pricing::TokenCostRates; -use pricing::{HAIKU_RATES, OPUS_4_1_RATES, OPUS_4_5_PLUS_RATES, SONNET_RATES}; +use pricing::{HAIKU_RATES, OPUS_4_5_PLUS_RATES, SONNET_RATES}; // ── ModelInfo ── @@ -155,19 +155,6 @@ pub(crate) const MODELS: &[ModelInfo] = &[ }, cost_rates: Some(HAIKU_RATES), }, - ModelInfo { - id_substr: "claude-opus-4-1", - display_name: "Claude Opus 4.1", - cutoff: Some("January 2025"), - capabilities: Capabilities { - interleaved_thinking: true, - context_management: true, - context_1m: false, - supported_efforts: &[], - structured_outputs: true, - }, - cost_rates: Some(OPUS_4_1_RATES), - }, ]; impl Capabilities { @@ -343,7 +330,6 @@ mod tests { "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5", - "claude-opus-4-1", ] { assert!( !lookup(other) @@ -564,6 +550,7 @@ mod tests { "claude-sonnet-4", "claude-haiku-4", "claude-opus-4-20250514", + "claude-opus-4-1", "gpt-4", ] { assert!(lookup(unknown).is_none(), "{unknown} must not resolve"); @@ -613,20 +600,6 @@ mod tests { assert!((one_hour - 40.5).abs() < 1e-9); } - #[test] - fn token_cost_rates_for_opus_4_1_uses_older_pricing() { - let rates = token_cost_rates_for("claude-opus-4-1").unwrap(); - let cost = rates.estimate_usd( - 1_000_000, - 1_000_000, - 1_000_000, - 1_000_000, - PromptCacheTtl::FiveMin, - ); - - assert!((cost - 110.25).abs() < 1e-9); - } - #[test] fn token_cost_rates_for_unknown_model_is_absent() { assert!(token_cost_rates_for("claude-future-9").is_none()); @@ -644,7 +617,6 @@ mod tests { ("claude-opus-4-5", "Claude Opus 4.5"), ("claude-sonnet-4-5", "Claude Sonnet 4.5"), ("claude-haiku-4-5", "Claude Haiku 4.5"), - ("claude-opus-4-1", "Claude Opus 4.1"), ] { assert_eq!(display_name(id), expected, "{id}"); } @@ -678,7 +650,6 @@ mod tests { ("claude-opus-4-7", "Opus 4.7"), ("claude-sonnet-4-6", "Sonnet 4.6"), ("claude-haiku-4-5", "Haiku 4.5"), - ("claude-opus-4-1", "Opus 4.1"), ] { assert_eq!(short_display_name(id), expected, "{id}"); } diff --git a/crates/oxide-code/src/model/pricing.rs b/crates/oxide-code/src/model/pricing.rs index 50ae6252..08cde7ec 100644 --- a/crates/oxide-code/src/model/pricing.rs +++ b/crates/oxide-code/src/model/pricing.rs @@ -24,14 +24,6 @@ pub(super) const OPUS_4_5_PLUS_RATES: TokenCostRates = TokenCostRates { output: 25.0, }; -pub(super) const OPUS_4_1_RATES: TokenCostRates = TokenCostRates { - input: 15.0, - cache_write_5m: 18.75, - cache_write_1h: 30.0, - cache_read: 1.50, - output: 75.0, -}; - pub(super) const SONNET_RATES: TokenCostRates = TokenCostRates { input: 3.0, cache_write_5m: 3.75, diff --git a/crates/oxide-code/src/slash/model.rs b/crates/oxide-code/src/slash/model.rs index c642724a..fc976756 100644 --- a/crates/oxide-code/src/slash/model.rs +++ b/crates/oxide-code/src/slash/model.rs @@ -470,7 +470,6 @@ mod tests { for dated in [ "claude-opus-4-7-20260101", "claude-opus-4-6-20250805", - "claude-opus-4-1-20250805", "claude-sonnet-4-5-20250929", ] { assert_eq!( diff --git a/docs/research/api/anthropic.md b/docs/research/api/anthropic.md index 7ca29f2d..a9b192c4 100644 --- a/docs/research/api/anthropic.md +++ b/docs/research/api/anthropic.md @@ -68,16 +68,16 @@ Rows are grouped by role: identity / auth → universal agentic → model-tier-g Cell legend: `✓` always on, `-` not supported (or stripped), `[1m]` opt-in via the model suffix, `*` caller opt-in (body field + beta ship together, see rules below). -| Beta | Opus 4 (base) | Opus 4.1 / 4.5 | Opus 4.6+ | Sonnet 4 (base) | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4 (base) | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | -| --------------------------------- | ------------- | -------------- | --------- | --------------- | ---------- | ----------- | -------------- | ------------------- | -------------------- | -| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | - | -| `context-1m-2025-08-07` | - | - | `[1m]` | `[1m]` | `[1m]` | `[1m]` | - | - | - | -| `effort-2025-11-24` | - | - | ✓ | - | - | ✓ | - | - | - | -| `structured-outputs-2025-12-15` | - | `*` | `*` | - | `*` | `*` | - | `*` | `*` | +| Beta | Opus 4 (base) | Opus 4.5 | Opus 4.6+ | Sonnet 4 (base) | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4 (base) | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | +| --------------------------------- | ------------- | -------- | --------- | --------------- | ---------- | ----------- | -------------- | ------------------- | -------------------- | +| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | - | +| `context-1m-2025-08-07` | - | - | `[1m]` | `[1m]` | `[1m]` | `[1m]` | - | - | - | +| `effort-2025-11-24` | - | - | ✓ | - | - | ✓ | - | - | - | +| `structured-outputs-2025-12-15` | - | `*` | `*` | - | `*` | `*` | - | `*` | `*` | Key rules: @@ -88,7 +88,7 @@ Key rules: - **`context-1m` is user opt-in via `[1m]`.** Appending `[1m]` to the model string (e.g., `claude-opus-4-8[1m]`) adds the 1M beta and strips the tag before the request hits the wire. Family-based auto-enable would 400 on subscriptions or gateways that do not carry 1M access. Convention matches Claude Code. - **`effort` is Opus 4.6+ and Sonnet 4.6+ only.** Opus 4.5 and older, Sonnet 4.5 and older, and all Haiku variants reject it per upstream's `modelSupportsEffort`. Per-model support is encoded in `Capabilities::supported_efforts`; `accepts_effort`, `clamp_effort`, and `default_effort` keep user picks and defaults inside that set. - **`effort` and `context-management` betas need a body field.** Sending the header alone is a silent no-op: the request runs at the server default. See [Agentic Request Body Fields](#agentic-request-body-fields) for the matching `output_config.effort` and `context_management.edits` shapes. oxide-code pairs each capability with both its beta and its body field so the two stay in sync. -- **`structured-outputs` is per-version and caller-opt-in.** The upstream allowlist is Opus 4.1 / 4.5 / 4.6+, Sonnet 4.5 / 4.6+, Haiku 4.5. The beta ships only when a caller supplies an `output_config.format` (today: the AI-title generator). The body field and header are paired on the same capability flag. A schema passed to an unsupported model silently falls back to free-form text, mirroring the `[1m]` × `context_1m` silent-strip pattern. +- **`structured-outputs` is per-version and caller-opt-in.** The upstream allowlist is Opus 4.5 / 4.6+, Sonnet 4.5 / 4.6+, Haiku 4.5. The beta ships only when a caller supplies an `output_config.format` (today: the AI-title generator). The body field and header are paired on the same capability flag. A schema passed to an unsupported model silently falls back to free-form text, mirroring the `[1m]` × `context_1m` silent-strip pattern. - **Unknown model aliases** fall through substring matching on the family stem. `claude-opus-5-x` would miss every row and ship with only the identity / caching betas. Bump the `MODELS` table when a new family lands. oxide-code gates each beta header on the target model in `client::anthropic::compute_betas`, which consults the ground-truth `Capabilities` flags in `crate::model::MODELS`. New models ship by adding a row to that table, with no beta-logic changes needed. diff --git a/docs/research/tui/status-line.md b/docs/research/tui/status-line.md index 4ea2ba05..568b91d8 100644 --- a/docs/research/tui/status-line.md +++ b/docs/research/tui/status-line.md @@ -42,7 +42,6 @@ Anthropic's pricing page lists first-party Claude API prices in USD per million | Family | Input | 5m cache write | 1h cache write | Cache read | Output | | -------------------------- | ----- | -------------- | -------------- | ---------- | ------ | | Opus 4.8 / 4.7 / 4.6 / 4.5 | $5 | $6.25 | $10 | $0.50 | $25 | -| Opus 4.1 | $15 | $18.75 | $30 | $1.50 | $75 | | Sonnet 4.x | $3 | $3.75 | $6 | $0.30 | $15 | | Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 | From 2726b38b79df6a16084711e4f0f87679cfda5857 Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 29 May 2026 17:03:36 +0800 Subject: [PATCH 3/5] docs(research): drop unmodeled base-4 columns from the beta matrix The beta matrix mirrors the families oxide-code's MODELS table actually sends. Base Opus 4, Sonnet 4, and Haiku 4 resolve to nothing in `lookup` (asserted by lookup_unknown_or_retired), so their columns documented profiles the client never emits. The 4.5 vs 4.6+ columns still carry the tier-gating contrast (effort, context-1m), and the unknown-alias fallback is already covered in prose. --- docs/research/api/anthropic.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/research/api/anthropic.md b/docs/research/api/anthropic.md index a9b192c4..c0cfa5c5 100644 --- a/docs/research/api/anthropic.md +++ b/docs/research/api/anthropic.md @@ -68,16 +68,16 @@ Rows are grouped by role: identity / auth → universal agentic → model-tier-g Cell legend: `✓` always on, `-` not supported (or stripped), `[1m]` opt-in via the model suffix, `*` caller opt-in (body field + beta ship together, see rules below). -| Beta | Opus 4 (base) | Opus 4.5 | Opus 4.6+ | Sonnet 4 (base) | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4 (base) | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | -| --------------------------------- | ------------- | -------- | --------- | --------------- | ---------- | ----------- | -------------- | ------------------- | -------------------- | -| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | - | -| `context-1m-2025-08-07` | - | - | `[1m]` | `[1m]` | `[1m]` | `[1m]` | - | - | - | -| `effort-2025-11-24` | - | - | ✓ | - | - | ✓ | - | - | - | -| `structured-outputs-2025-12-15` | - | `*` | `*` | - | `*` | `*` | - | `*` | `*` | +| Beta | Opus 4.5 | Opus 4.6+ | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | +| --------------------------------- | -------- | --------- | ---------- | ----------- | ------------------- | -------------------- | +| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | - | - | +| `context-1m-2025-08-07` | - | `[1m]` | `[1m]` | `[1m]` | - | - | +| `effort-2025-11-24` | - | ✓ | - | ✓ | - | - | +| `structured-outputs-2025-12-15` | `*` | `*` | `*` | `*` | `*` | `*` | Key rules: From 29265e43e2c202e331dde236c841913c1312215a Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 29 May 2026 17:40:22 +0800 Subject: [PATCH 4/5] feat(model): default effort per model, mirroring Claude Code The highest-non-Max heuristic resolved both Opus 4.7 and 4.8 to xhigh, but upstream defaults every effort-capable model to `high` and Claude Code bumps only 4.7 to `xhigh` (4.8 rides the `high` default). Replace the heuristic with an explicit per-model `default_effort` on Capabilities: xhigh on 4.7, high on 4.8 / 4.6 / Sonnet 4.6, none on models without effort. The default model (Opus 4.8) now resolves to `high`, so derived max_tokens is 32K. --- crates/oxide-code/src/config.rs | 14 ++++---- crates/oxide-code/src/model.rs | 57 ++++++++++++++++----------------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/crates/oxide-code/src/config.rs b/crates/oxide-code/src/config.rs index df31713c..210b36cf 100644 --- a/crates/oxide-code/src/config.rs +++ b/crates/oxide-code/src/config.rs @@ -879,15 +879,15 @@ mod tests { #[tokio::test] async fn load_defaults_apply_when_no_config_and_no_env() { - // Opus 4.8 supports `xhigh`; `effort` / `max_tokens` derive from that ceiling. + // Opus 4.8 defaults to `high` (mirroring Claude Code); `max_tokens` derives from that. let dir = tempfile::tempdir().unwrap(); let config = temp_env::async_with_vars(env_vars(vec![xdg(&dir)]), Config::load()) .await .unwrap(); assert_eq!(config.model, DEFAULT_MODEL); assert_eq!(config.base_url, DEFAULT_BASE_URL); - assert_eq!(config.max_tokens, 64_000); - assert_eq!(config.effort, Some(Effort::Xhigh)); + assert_eq!(config.max_tokens, 32_000); + assert_eq!(config.effort, Some(Effort::High)); assert_eq!(config.prompt_cache_ttl, PromptCacheTtl::OneHour); assert!(config.compaction.auto.enabled); assert_eq!( @@ -1505,13 +1505,11 @@ mod tests { // ── Config::load / effort resolution ── #[tokio::test] - async fn load_effort_default_follows_model_ceiling() { + async fn load_effort_defaults_to_per_model_value() { + // 4.7 ships xhigh, 4.8 rides the high default; no-effort models stay unset. for (model, expected) in [ - ("claude-opus-4-8", Some(Effort::Xhigh)), ("claude-opus-4-7", Some(Effort::Xhigh)), - ("claude-opus-4-6", Some(Effort::High)), - ("claude-sonnet-4-6", Some(Effort::High)), - ("claude-sonnet-4-5", None), + ("claude-opus-4-8", Some(Effort::High)), ("claude-haiku-4-5", None), ] { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/oxide-code/src/model.rs b/crates/oxide-code/src/model.rs index d1d9aa40..311898b1 100644 --- a/crates/oxide-code/src/model.rs +++ b/crates/oxide-code/src/model.rs @@ -43,6 +43,10 @@ pub(crate) struct Capabilities { pub(crate) context_1m: bool, /// `output_config.effort` levels accepted upstream. Empty when the model rejects `effort`. pub(crate) supported_efforts: &'static [Effort], + /// Effort sent when the user picks none. Mirrors Claude Code's per-model stock default, which + /// differs from a model's ceiling (Opus 4.7 ships `xhigh`, Opus 4.8 rides the `high` default). + /// `None` when the model rejects `effort`. + pub(crate) default_effort: Option, /// `structured-outputs-2025-12-15` beta. pub(crate) structured_outputs: bool, } @@ -66,6 +70,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ Effort::Xhigh, Effort::Max, ], + default_effort: Some(Effort::High), structured_outputs: true, }, cost_rates: Some(OPUS_4_5_PLUS_RATES), @@ -85,6 +90,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ Effort::Xhigh, Effort::Max, ], + default_effort: Some(Effort::Xhigh), structured_outputs: true, }, cost_rates: Some(OPUS_4_5_PLUS_RATES), @@ -98,6 +104,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: true, supported_efforts: &[Effort::Low, Effort::Medium, Effort::High, Effort::Max], + default_effort: Some(Effort::High), structured_outputs: true, }, cost_rates: Some(OPUS_4_5_PLUS_RATES), @@ -111,6 +118,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: true, supported_efforts: &[Effort::Low, Effort::Medium, Effort::High], + default_effort: Some(Effort::High), structured_outputs: true, }, cost_rates: Some(SONNET_RATES), @@ -124,6 +132,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: false, supported_efforts: &[], + default_effort: None, structured_outputs: true, }, cost_rates: Some(OPUS_4_5_PLUS_RATES), @@ -137,6 +146,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: true, supported_efforts: &[], + default_effort: None, structured_outputs: true, }, cost_rates: Some(SONNET_RATES), @@ -151,6 +161,7 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: false, supported_efforts: &[], + default_effort: None, structured_outputs: true, }, cost_rates: Some(HAIKU_RATES), @@ -177,21 +188,11 @@ impl Capabilities { .find(|&level| level <= pick) } - /// Default tier when the user hasn't picked one. `Max` is opt-in, so the implicit ceiling is - /// the highest non-`Max` supported level. - pub(crate) fn default_effort(self) -> Option { - self.supported_efforts - .iter() - .copied() - .rev() - .find(|&level| level != Effort::Max) - } - - /// Clamps `pick` when present, otherwise falls back to [`Self::default_effort`]. + /// Clamps `pick` when present, otherwise falls back to the model's stock `default_effort`. pub(crate) fn resolve_effort(self, pick: Option) -> Option { match pick { Some(p) => self.clamp_effort(p), - None => self.default_effort(), + None => self.default_effort, } } } @@ -463,22 +464,16 @@ mod tests { // ── Capabilities::default_effort ── #[test] - fn default_effort_picks_highest_supported_tier_when_user_has_no_pick() { - // Opus 4.7 / 4.8: full ladder → xhigh. - for id in ["claude-opus-4-7", "claude-opus-4-8"] { - let caps = lookup(id).unwrap().capabilities; - assert_eq!(caps.default_effort(), Some(Effort::Xhigh), "{id}"); - } - - // Opus 4.6 / Sonnet 4.6: effort but no xhigh → high. - for id in ["claude-opus-4-6", "claude-sonnet-4-6"] { - let caps = lookup(id).unwrap().capabilities; - assert_eq!(caps.default_effort(), Some(Effort::High), "{id}"); + fn default_effort_stays_within_supported_efforts() { + // Each row's stock default must be a tier the model accepts (or `None` when it rejects + // effort), so resolve_effort never emits a level the API would 400 on. + for info in MODELS { + let caps = info.capabilities; + match caps.default_effort { + Some(level) => assert!(caps.accepts_effort(level), "{}", info.id_substr), + None => assert!(!caps.has_effort(), "{}", info.id_substr), + } } - - // No effort tier at all → None. - let haiku_4_5 = lookup("claude-haiku-4-5").unwrap().capabilities; - assert_eq!(haiku_4_5.default_effort(), None); } // ── Capabilities::resolve_effort ── @@ -503,11 +498,13 @@ mod tests { } #[test] - fn resolve_effort_falls_back_to_model_default_when_pick_is_none() { + fn resolve_effort_falls_back_to_per_model_default_when_pick_is_none() { + // 4.7 and 4.8 share the full ladder but default differently: 4.7 ships xhigh, 4.8 rides + // the high default (mirrors Claude Code). let opus_4_7 = lookup("claude-opus-4-7").unwrap().capabilities; assert_eq!(opus_4_7.resolve_effort(None), Some(Effort::Xhigh)); - let sonnet_4_6 = lookup("claude-sonnet-4-6").unwrap().capabilities; - assert_eq!(sonnet_4_6.resolve_effort(None), Some(Effort::High)); + let opus_4_8 = lookup("claude-opus-4-8").unwrap().capabilities; + assert_eq!(opus_4_8.resolve_effort(None), Some(Effort::High)); } #[test] From ccf1789b4f210ce8b98efc2153c7ffb98c73a1df Mon Sep 17 00:00:00 2001 From: Hakula Chen Date: Fri, 29 May 2026 17:40:22 +0800 Subject: [PATCH 5/5] docs: keep upstream model coverage and fix effort references The API and status-line research docs describe upstream rather than oxide-code's supported set, so restore Opus 4.1 and the base-4 columns in the beta matrix and pricing table (oxide-code still drops 4.1 from its own model table). Collapse contiguous version ranges to the `4.1+` / `4.5+` shorthand. Correct the effort docs: the per-model default note now states the real upstream default (`high` everywhere, with Claude Code bumping 4.7 to `xhigh`), the configuration guide's default table matches, and the tier-guide link points to the effort doc that carries the table instead of a mislabeled migration-guide link. --- docs/guide/configuration.md | 8 +++++--- docs/research/api/anthropic.md | 24 ++++++++++++------------ docs/research/tui/status-line.md | 11 ++++++----- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index c255d563..f6e04f54 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -59,12 +59,14 @@ Per-model defaults when `effort` is unset: | Model | Default | | --------------- | ------- | -| Opus 4.8 / 4.7 | `xhigh` | -| Opus 4.6 | `high` | +| Opus 4.7 | `xhigh` | +| Opus 4.8 / 4.6 | `high` | | Sonnet 4.6 | `high` | | Everything else | (unset) | -Tier guide (from the [Opus 4.8 migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide)): +oxide-code mirrors Claude Code here: `high` is the upstream API default, and Opus 4.7 ships `xhigh`. `xhigh` stays the recommended pick for coding on both 4.7 and 4.8, so set it explicitly (or via `/effort`) when you want it on 4.8. + +Tier guide (from Anthropic's [effort documentation](https://platform.claude.com/docs/en/build-with-claude/effort)): - `max`: Deepest reasoning, Opus-only, with diminishing returns on some tasks. - `xhigh`: Recommended default for coding and agentic work on Opus 4.7 / 4.8. diff --git a/docs/research/api/anthropic.md b/docs/research/api/anthropic.md index c0cfa5c5..1c676e78 100644 --- a/docs/research/api/anthropic.md +++ b/docs/research/api/anthropic.md @@ -68,16 +68,16 @@ Rows are grouped by role: identity / auth → universal agentic → model-tier-g Cell legend: `✓` always on, `-` not supported (or stripped), `[1m]` opt-in via the model suffix, `*` caller opt-in (body field + beta ship together, see rules below). -| Beta | Opus 4.5 | Opus 4.6+ | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | -| --------------------------------- | -------- | --------- | ---------- | ----------- | ------------------- | -------------------- | -| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | - | - | -| `context-1m-2025-08-07` | - | `[1m]` | `[1m]` | `[1m]` | - | - | -| `effort-2025-11-24` | - | ✓ | - | ✓ | - | - | -| `structured-outputs-2025-12-15` | `*` | `*` | `*` | `*` | `*` | `*` | +| Beta | Opus 4 (base) | Opus 4.1 / 4.5 | Opus 4.6+ | Sonnet 4 (base) | Sonnet 4.5 | Sonnet 4.6+ | Haiku 4 (base) | Haiku 4.5 (agentic) | Haiku 4.5 (one-shot) | +| --------------------------------- | ------------- | -------------- | --------- | --------------- | ---------- | ----------- | -------------- | ------------------- | -------------------- | +| `claude-code-20250219` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `oauth-2025-04-20` (OAuth only) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `context-management-2025-06-27` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `prompt-caching-scope-2026-01-05` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | +| `interleaved-thinking-2025-05-14` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | - | - | - | +| `context-1m-2025-08-07` | - | - | `[1m]` | `[1m]` | `[1m]` | `[1m]` | - | - | - | +| `effort-2025-11-24` | - | - | ✓ | - | - | ✓ | - | - | - | +| `structured-outputs-2025-12-15` | - | `*` | `*` | - | `*` | `*` | - | `*` | `*` | Key rules: @@ -88,7 +88,7 @@ Key rules: - **`context-1m` is user opt-in via `[1m]`.** Appending `[1m]` to the model string (e.g., `claude-opus-4-8[1m]`) adds the 1M beta and strips the tag before the request hits the wire. Family-based auto-enable would 400 on subscriptions or gateways that do not carry 1M access. Convention matches Claude Code. - **`effort` is Opus 4.6+ and Sonnet 4.6+ only.** Opus 4.5 and older, Sonnet 4.5 and older, and all Haiku variants reject it per upstream's `modelSupportsEffort`. Per-model support is encoded in `Capabilities::supported_efforts`; `accepts_effort`, `clamp_effort`, and `default_effort` keep user picks and defaults inside that set. - **`effort` and `context-management` betas need a body field.** Sending the header alone is a silent no-op: the request runs at the server default. See [Agentic Request Body Fields](#agentic-request-body-fields) for the matching `output_config.effort` and `context_management.edits` shapes. oxide-code pairs each capability with both its beta and its body field so the two stay in sync. -- **`structured-outputs` is per-version and caller-opt-in.** The upstream allowlist is Opus 4.5 / 4.6+, Sonnet 4.5 / 4.6+, Haiku 4.5. The beta ships only when a caller supplies an `output_config.format` (today: the AI-title generator). The body field and header are paired on the same capability flag. A schema passed to an unsupported model silently falls back to free-form text, mirroring the `[1m]` × `context_1m` silent-strip pattern. +- **`structured-outputs` is per-version and caller-opt-in.** The upstream allowlist is Opus 4.1+, Sonnet 4.5+, Haiku 4.5. The beta ships only when a caller supplies an `output_config.format` (today: the AI-title generator). The body field and header are paired on the same capability flag. A schema passed to an unsupported model silently falls back to free-form text, mirroring the `[1m]` × `context_1m` silent-strip pattern. - **Unknown model aliases** fall through substring matching on the family stem. `claude-opus-5-x` would miss every row and ship with only the identity / caching betas. Bump the `MODELS` table when a new family lands. oxide-code gates each beta header on the target model in `client::anthropic::compute_betas`, which consults the ground-truth `Capabilities` flags in `crate::model::MODELS`. New models ship by adding a row to that table, with no beta-logic changes needed. @@ -238,7 +238,7 @@ GA as of Opus 4.6. Controls the intelligence-vs-latency tier of agentic turns vi - **The `effort-2025-11-24` beta header is necessary but not sufficient.** oxide-code used to send the header without the body field. The header became a no-op and the model ran at an undefined default. - **Per-model ceiling.** `max` is Opus-only, and Sonnet 4.6 400s on it. `xhigh` is Opus 4.7 / 4.8-only. `Capabilities::supported_efforts` encodes the allowed set, and `Capabilities::clamp_effort` clamps a user pick down to the highest supported level at or below it. -- **Per-model default.** `Capabilities::default_effort` picks the highest non-`max` tier: `xhigh` on Opus 4.7 / 4.8, `high` on Opus 4.6 and Sonnet 4.6, omitted on models without effort. This tracks Claude Code for 4.6 / 4.7; oxide-code keeps `xhigh` as the 4.8 default even though upstream lowered the stock 4.8 default to `high`. +- **Per-model default.** The raw API default is `high` for every effort-capable model (`high` is equivalent to omitting the field), and `xhigh` is Anthropic's coding recommendation rather than a stock default. Claude Code bumps Opus 4.7 to `xhigh` but rides the `high` default on Opus 4.8; oxide-code mirrors this in `Capabilities::default_effort` (`xhigh` on 4.7, `high` on 4.8 / 4.6 / Sonnet 4.6, omitted on models without effort). - **`max_tokens` should scale with effort.** Claude Code uses 64 K on Opus 4.7 at `xhigh`, 32 K on Sonnet 4.6 at `high`. oxide-code's `default_max_tokens(effort)` matches the upper tiers and uses 16 K otherwise when the user hasn't set `ANTHROPIC_MAX_TOKENS` explicitly. ### `context_management.edits` diff --git a/docs/research/tui/status-line.md b/docs/research/tui/status-line.md index 568b91d8..f71c8f8f 100644 --- a/docs/research/tui/status-line.md +++ b/docs/research/tui/status-line.md @@ -39,11 +39,12 @@ opencode keeps usage in the prompt footer rather than a fully configurable statu Anthropic's pricing page lists first-party Claude API prices in USD per million tokens and separate prompt-cache rates for 5-minute writes, 1-hour writes, cache reads, and output tokens. Checked on 2026-05-14, the relevant rows for oxide-code's model table are: -| Family | Input | 5m cache write | 1h cache write | Cache read | Output | -| -------------------------- | ----- | -------------- | -------------- | ---------- | ------ | -| Opus 4.8 / 4.7 / 4.6 / 4.5 | $5 | $6.25 | $10 | $0.50 | $25 | -| Sonnet 4.x | $3 | $3.75 | $6 | $0.30 | $15 | -| Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 | +| Family | Input | 5m cache write | 1h cache write | Cache read | Output | +| ---------- | ----- | -------------- | -------------- | ---------- | ------ | +| Opus 4.5+ | $5 | $6.25 | $10 | $0.50 | $25 | +| Opus 4.1 | $15 | $18.75 | $30 | $1.50 | $75 | +| Sonnet 4.x | $3 | $3.75 | $6 | $0.30 | $15 | +| Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 | Cost display should stay best-effort because account discounts, marketplace billing, data residency, fast mode, and server-side tool pricing can change the final bill.