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..210b36cf 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,15 +879,15 @@ 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 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,12 +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-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 f17ef0a1..311898b1 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 ── @@ -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, } @@ -51,6 +55,26 @@ 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, + ], + default_effort: Some(Effort::High), + structured_outputs: true, + }, + cost_rates: Some(OPUS_4_5_PLUS_RATES), + }, ModelInfo { id_substr: "claude-opus-4-7", display_name: "Claude Opus 4.7", @@ -66,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), @@ -79,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), @@ -92,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), @@ -105,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), @@ -118,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), @@ -132,23 +161,11 @@ pub(crate) const MODELS: &[ModelInfo] = &[ context_management: true, context_1m: false, supported_efforts: &[], + default_effort: None, structured_outputs: true, }, 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 { @@ -171,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, } } } @@ -279,10 +286,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 +313,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", @@ -322,21 +331,20 @@ mod tests { "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5", - "claude-opus-4-1", ] { assert!( !lookup(other) .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 +386,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", @@ -455,20 +464,16 @@ mod tests { // ── Capabilities::default_effort ── #[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.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 ── @@ -493,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] @@ -540,6 +547,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"); @@ -589,20 +597,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()); @@ -613,13 +607,13 @@ 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"), ("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}"); } @@ -649,10 +643,10 @@ 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"), - ("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/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..fc976756 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") ); } @@ -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!( @@ -489,7 +488,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 +500,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 +542,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..f6e04f54 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 | @@ -60,14 +60,16 @@ Per-model defaults when `effort` is unset: | Model | Default | | --------------- | ------- | | Opus 4.7 | `xhigh` | -| Opus 4.6 | `high` | +| Opus 4.8 / 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)): +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. +- `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 +122,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 +135,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 +146,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 +198,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..1c676e78 100644 --- a/docs/research/api/anthropic.md +++ b/docs/research/api/anthropic.md @@ -85,10 +85,10 @@ 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. +- **`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. @@ -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.** 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 066f6358..f71c8f8f 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.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.