diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs index 30f1a383a..f5a707c4b 100644 --- a/lib/crates/fabro-agent/src/compaction.rs +++ b/lib/crates/fabro-agent/src/compaction.rs @@ -113,7 +113,7 @@ function names, error messages, and exact values. Omit pleasantries and conversa tools: None, tool_choice: None, response_format: None, - temperature: Some(0.0), + temperature: None, top_p: None, max_tokens: Some(4096), stop_sequences: None, diff --git a/lib/crates/fabro-cli/tests/it/cmd/model.rs b/lib/crates/fabro-cli/tests/it/cmd/model.rs index 67e6b5f19..de7c3feeb 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/model.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/model.rs @@ -38,7 +38,8 @@ fn bare() { exit_code: 0 ----- stdout ----- MODEL PROVIDER ALIASES CONTEXT COST SPEED - claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-7 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-6 anthropic 1m $5.0 / $25.0 25 tok/s claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s @@ -72,7 +73,8 @@ fn list() { exit_code: 0 ----- stdout ----- MODEL PROVIDER ALIASES CONTEXT COST SPEED - claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-7 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-6 anthropic 1m $5.0 / $25.0 25 tok/s claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s @@ -106,7 +108,8 @@ fn list_provider() { exit_code: 0 ----- stdout ----- MODEL PROVIDER ALIASES CONTEXT COST SPEED - claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-7 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-6 anthropic 1m $5.0 / $25.0 25 tok/s claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s @@ -124,7 +127,8 @@ fn list_query() { exit_code: 0 ----- stdout ----- MODEL PROVIDER ALIASES CONTEXT COST SPEED - claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-7 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-6 anthropic 1m $5.0 / $25.0 25 tok/s ----- stderr ----- "); } @@ -156,7 +160,8 @@ fn list_query_case_insensitive() { exit_code: 0 ----- stdout ----- MODEL PROVIDER ALIASES CONTEXT COST SPEED - claude-opus-4-6 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-7 anthropic opus, claude-opus 1m $5.0 / $25.0 25 tok/s + claude-opus-4-6 anthropic 1m $5.0 / $25.0 25 tok/s ----- stderr ----- "); } diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index aa96e021a..bfc1b7014 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -544,6 +544,8 @@ fn effort_to_budget_tokens(effort: &str, max_tokens: i64) -> i64 { let budget = match effort { "low" => max_tokens / 4, "high" => max_tokens * 3 / 4, + "xhigh" => max_tokens * 7 / 8, + "max" => max_tokens, _ => max_tokens / 2, // "medium" or unknown }; // Anthropic requires budget_tokens >= 1024 @@ -2231,4 +2233,36 @@ mod tests { "should contain fast-mode header" ); } + + #[test] + fn effort_to_budget_tokens_xhigh_maps_to_seven_eighths() { + assert_eq!(effort_to_budget_tokens("xhigh", 16_000), 14_000); + } + + #[test] + fn effort_to_budget_tokens_max_maps_to_full_budget() { + assert_eq!(effort_to_budget_tokens("max", 16_000), 16_000); + } + + #[test] + fn build_api_request_falls_back_to_thinking_budget_for_non_effort_model() { + let adapter = Adapter::new("test-key"); + let request = Request { + model: "claude-sonnet-4-5".to_string(), + max_tokens: Some(16_000), + reasoning_effort: Some(ReasoningEffort::XHigh), + ..make_base_request() + }; + + let (api_request, _req_builder) = build_api_request(&adapter, &request, false); + assert!( + api_request.output_config.is_none(), + "non-effort models must not receive output_config" + ); + let thinking = api_request + .thinking + .expect("thinking must be set for fallback path"); + assert_eq!(thinking["type"], "enabled"); + assert_eq!(thinking["budget_tokens"], 14_000); + } } diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index 8a4bb1c79..89255b90b 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -417,6 +417,8 @@ pub enum ReasoningEffort { Low, Medium, High, + XHigh, + Max, } impl ReasoningEffort { @@ -425,6 +427,8 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "medium", Self::High => "high", + Self::XHigh => "xhigh", + Self::Max => "max", } } } @@ -442,8 +446,10 @@ impl std::str::FromStr for ReasoningEffort { "low" => Ok(Self::Low), "medium" => Ok(Self::Medium), "high" => Ok(Self::High), + "xhigh" => Ok(Self::XHigh), + "max" => Ok(Self::Max), other => Err(format!( - "invalid reasoning_effort: {other:?} (expected low, medium, or high)" + "invalid reasoning_effort: {other:?} (expected low, medium, high, xhigh, or max)" )), } } @@ -1292,4 +1298,32 @@ mod tests { fn tool_choice_mode_str_named() { assert_eq!(ToolChoice::named("get_weather").mode_str(), "named"); } + + #[test] + fn reasoning_effort_from_str_round_trip() { + use std::str::FromStr; + assert_eq!(ReasoningEffort::from_str("low"), Ok(ReasoningEffort::Low)); + assert_eq!( + ReasoningEffort::from_str("medium"), + Ok(ReasoningEffort::Medium) + ); + assert_eq!(ReasoningEffort::from_str("high"), Ok(ReasoningEffort::High)); + assert_eq!( + ReasoningEffort::from_str("xhigh"), + Ok(ReasoningEffort::XHigh) + ); + assert_eq!(ReasoningEffort::from_str("max"), Ok(ReasoningEffort::Max)); + assert_eq!(ReasoningEffort::XHigh.as_str(), "xhigh"); + assert_eq!(ReasoningEffort::Max.as_str(), "max"); + } + + #[test] + fn reasoning_effort_from_str_rejects_unknown_with_updated_error() { + use std::str::FromStr; + let err = ReasoningEffort::from_str("bogus").expect_err("should reject"); + assert!( + err.contains("low, medium, high, xhigh, or max"), + "error should list all accepted levels, got: {err}" + ); + } } diff --git a/lib/crates/fabro-model/src/billing.rs b/lib/crates/fabro-model/src/billing.rs index 2e16353d9..6ba62b088 100644 --- a/lib/crates/fabro-model/src/billing.rs +++ b/lib/crates/fabro-model/src/billing.rs @@ -389,22 +389,26 @@ impl Model { .map(PricePerMTok::from_usd); let (input, output, cached_input) = match (self.provider, speed) { - (Provider::Anthropic, Some(Speed::Fast)) if self.id == "claude-opus-4-6" => ( - input.multiply_ratio( - ANTHROPIC_FAST_MODE_MULTIPLIER_NUMERATOR, - ANTHROPIC_FAST_MODE_MULTIPLIER_DENOMINATOR, - ), - output.multiply_ratio( - ANTHROPIC_FAST_MODE_MULTIPLIER_NUMERATOR, - ANTHROPIC_FAST_MODE_MULTIPLIER_DENOMINATOR, - ), - cached_input.map(|rate| { - rate.multiply_ratio( + (Provider::Anthropic, Some(Speed::Fast)) + if self.id == "claude-opus-4-7" || self.id == "claude-opus-4-6" => + { + ( + input.multiply_ratio( ANTHROPIC_FAST_MODE_MULTIPLIER_NUMERATOR, ANTHROPIC_FAST_MODE_MULTIPLIER_DENOMINATOR, - ) - }), - ), + ), + output.multiply_ratio( + ANTHROPIC_FAST_MODE_MULTIPLIER_NUMERATOR, + ANTHROPIC_FAST_MODE_MULTIPLIER_DENOMINATOR, + ), + cached_input.map(|rate| { + rate.multiply_ratio( + ANTHROPIC_FAST_MODE_MULTIPLIER_NUMERATOR, + ANTHROPIC_FAST_MODE_MULTIPLIER_DENOMINATOR, + ) + }), + ) + } (_, None | Some(Speed::Standard)) => (input, output, cached_input), _ => return None, }; diff --git a/lib/crates/fabro-model/src/catalog.json b/lib/crates/fabro-model/src/catalog.json index bb6b2a154..e72f81a02 100644 --- a/lib/crates/fabro-model/src/catalog.json +++ b/lib/crates/fabro-model/src/catalog.json @@ -1,4 +1,21 @@ [ + { + "id": "claude-opus-4-7", + "provider": "anthropic", + "family": "claude-4", + "display_name": "Claude Opus 4.7", + "limits": { "context_window": 1000000, "max_output": 128000 }, + "training": "2025-08-01", + "knowledge_cutoff": "May 2025", + "features": { "tools": true, "vision": true, "reasoning": true, "effort": true }, + "costs": { + "input_cost_per_mtok": 5.0, + "output_cost_per_mtok": 25.0, + "cache_input_cost_per_mtok": 0.50 + }, + "estimated_output_tps": 25, + "aliases": ["opus", "claude-opus"] + }, { "id": "claude-opus-4-6", "provider": "anthropic", @@ -14,7 +31,7 @@ "cache_input_cost_per_mtok": 0.50 }, "estimated_output_tps": 25, - "aliases": ["opus", "claude-opus"] + "aliases": [] }, { "id": "claude-sonnet-4-5", diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index 38f7b39a0..ef96f2b2a 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -187,7 +187,7 @@ mod tests { #[test] fn builtin_get_by_alias() { let m = Catalog::builtin().get("opus").unwrap(); - assert_eq!(m.id, "claude-opus-4-6"); + assert_eq!(m.id, "claude-opus-4-7"); } #[test] @@ -485,10 +485,7 @@ mod tests { estimated_output_tps: Some( 25.0, ), - aliases: [ - "opus", - "claude-opus", - ], + aliases: [], default: false, } "#); @@ -498,7 +495,7 @@ mod tests { fn get_model_info_by_alias() { assert_eq!( Catalog::builtin().get("opus").unwrap().id, - "claude-opus-4-6" + "claude-opus-4-7" ); assert_eq!( Catalog::builtin().get("sonnet").unwrap().id, diff --git a/lib/crates/fabro-model/src/types.rs b/lib/crates/fabro-model/src/types.rs index 57d89b545..4eafcdc96 100644 --- a/lib/crates/fabro-model/src/types.rs +++ b/lib/crates/fabro-model/src/types.rs @@ -129,11 +129,11 @@ mod tests { #[test] fn inherent_methods_return_correct_values() { - let info = Catalog::builtin().get("claude-opus-4-6").unwrap(); - assert_eq!(info.id(), "claude-opus-4-6"); + let info = Catalog::builtin().get("claude-opus-4-7").unwrap(); + assert_eq!(info.id(), "claude-opus-4-7"); assert_eq!(info.provider(), Provider::Anthropic); assert_eq!(info.family(), "claude-4"); - assert_eq!(info.display_name(), "Claude Opus 4.6"); + assert_eq!(info.display_name(), "Claude Opus 4.7"); assert_eq!(info.context_window(), 1_000_000); assert_eq!(info.max_output(), Some(128_000)); assert!(info.supports_tools());