This commit is contained in:
Bryan Helmkamp 2026-04-16 13:13:09 -04:00
parent 74578c22bc
commit 1dd59ceea5
No known key found for this signature in database
8 changed files with 122 additions and 31 deletions

View file

@ -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,

View file

@ -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 -----
");
}

View file

@ -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);
}
}

View file

@ -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}"
);
}
}

View file

@ -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,
};

View file

@ -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",

View file

@ -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,

View file

@ -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());