Fix Sonnet 4.5 effort parameter error by converting to thinking API

claude-sonnet-4-5 doesn't support output_config.effort — it needs the
older thinking API with budget_tokens. Add an `effort` feature flag to
ModelFeatures and have the Anthropic adapter auto-convert reasoning_effort
to a thinking config for models that lack effort support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-21 11:44:15 -04:00
parent f0849a796a
commit 3229e58a7e
No known key found for this signature in database
3 changed files with 70 additions and 26 deletions

View file

@ -523,6 +523,18 @@ fn extract_thinking_config(
.cloned()
}
/// Map a reasoning effort level to a thinking `budget_tokens` value for models
/// that don't support the `output_config.effort` parameter (e.g. claude-sonnet-4-5).
fn effort_to_budget_tokens(effort: &str, max_tokens: i64) -> i64 {
let budget = match effort {
"low" => max_tokens / 4,
"high" => max_tokens * 3 / 4,
_ => max_tokens / 2, // "medium" or unknown
};
// Anthropic requires budget_tokens >= 1024
budget.max(1024)
}
fn is_auto_cache_enabled(provider_options: Option<&serde_json::Value>) -> bool {
provider_options
.and_then(|opts| opts.get("anthropic"))
@ -1072,22 +1084,48 @@ fn build_api_request(
apply_cache_control_to_conversation_prefix(&mut api_messages);
}
let thinking = extract_thinking_config(request.provider_options.as_ref());
let explicit_thinking = extract_thinking_config(request.provider_options.as_ref());
let output_config = request
.reasoning_effort
.as_ref()
.map(|effort| serde_json::json!({"effort": effort}));
// Check whether this model supports the `output_config.effort` parameter.
// Older reasoning models (e.g. claude-sonnet-4-5) need `thinking` with
// `budget_tokens` instead.
let model_info = fabro_model::get_model_info(&request.model);
let supports_effort = model_info.as_ref().is_none_or(|m| m.features.effort);
let mut resolved_max_tokens = request
.max_tokens
.or_else(|| model_info.as_ref().and_then(|m| m.limits.max_output))
.unwrap_or(65536);
let (thinking, output_config) = if let Some(effort) = &request.reasoning_effort {
if supports_effort {
(
explicit_thinking,
Some(serde_json::json!({"effort": effort})),
)
} else if explicit_thinking.is_none() {
// Convert effort level to a thinking budget for models that don't
// support the effort parameter (e.g. claude-sonnet-4-5).
let budget = effort_to_budget_tokens(effort, resolved_max_tokens);
if resolved_max_tokens <= budget {
resolved_max_tokens = budget + 1024;
}
(
Some(serde_json::json!({"type": "enabled", "budget_tokens": budget})),
None,
)
} else {
// thinking already configured via provider_options; skip output_config
(explicit_thinking, None)
}
} else {
(explicit_thinking, None)
};
let api_request = ApiRequest {
model: request.model.clone(),
messages: api_messages,
max_tokens: request
.max_tokens
.or_else(|| {
fabro_model::get_model_info(&request.model).and_then(|m| m.limits.max_output)
})
.unwrap_or(65536),
max_tokens: resolved_max_tokens,
system: system_value,
temperature: request.temperature,
top_p: request.top_p,

View file

@ -6,7 +6,7 @@
"display_name": "Claude Opus 4.6",
"limits": { "context_window": 1000000, "max_output": 128000 },
"training": "2025-08-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 15.0,
"output_cost_per_mtok": 75.0,
@ -38,7 +38,7 @@
"display_name": "Claude Sonnet 4.6",
"limits": { "context_window": 200000, "max_output": 64000 },
"training": "2025-08-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 3.0,
"output_cost_per_mtok": 15.0,
@ -71,7 +71,7 @@
"display_name": "GPT-5.2",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
"output_cost_per_mtok": 14.0,
@ -87,7 +87,7 @@
"display_name": "GPT-5 Mini",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,
"output_cost_per_mtok": 2.0,
@ -103,7 +103,7 @@
"display_name": "GPT-5.2 Codex",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
"output_cost_per_mtok": 14.0,
@ -119,7 +119,7 @@
"display_name": "GPT-5.3 Codex",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 1.75,
"output_cost_per_mtok": 14.0,
@ -135,7 +135,7 @@
"display_name": "GPT-5.3 Codex Spark",
"limits": { "context_window": 131072, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": false, "reasoning": true },
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": null,
"output_cost_per_mtok": null,
@ -151,7 +151,7 @@
"display_name": "GPT-5.4",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.5,
"output_cost_per_mtok": 15.0,
@ -168,7 +168,7 @@
"display_name": "GPT-5.4 Pro",
"limits": { "context_window": 1047576, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 30.0,
"output_cost_per_mtok": 180.0,
@ -184,7 +184,7 @@
"display_name": "GPT-5.4 Mini",
"limits": { "context_window": 400000, "max_output": 128000 },
"training": "2025-08-31",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.75,
"output_cost_per_mtok": 4.50,
@ -200,7 +200,7 @@
"display_name": "Gemini 3.1 Pro (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.0,
"output_cost_per_mtok": 12.0,
@ -217,7 +217,7 @@
"display_name": "Gemini 3.1 Pro Custom Tools (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 2.0,
"output_cost_per_mtok": 12.0,
@ -233,7 +233,7 @@
"display_name": "Gemini 3 Flash (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.5,
"output_cost_per_mtok": 3.0,
@ -249,7 +249,7 @@
"display_name": "Gemini 3.1 Flash Lite (Preview)",
"limits": { "context_window": 1048576, "max_output": 65536 },
"training": "2025-01-01",
"features": { "tools": true, "vision": true, "reasoning": true },
"features": { "tools": true, "vision": true, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,
"output_cost_per_mtok": 1.5,
@ -316,7 +316,7 @@
"display_name": "Mercury 2",
"limits": { "context_window": 131072, "max_output": 50000 },
"training": null,
"features": { "tools": true, "vision": false, "reasoning": true },
"features": { "tools": true, "vision": false, "reasoning": true, "effort": true },
"costs": {
"input_cost_per_mtok": 0.25,
"output_cost_per_mtok": 0.75,

View file

@ -13,6 +13,12 @@ pub struct ModelFeatures {
pub tools: bool,
pub vision: bool,
pub reasoning: bool,
/// Whether the model supports the `reasoning_effort` / `effort` parameter
/// directly (e.g. Anthropic `output_config.effort`, OpenAI `reasoning.effort`).
/// Models with `reasoning=true` but `effort=false` (e.g. claude-sonnet-4-5)
/// need the older `thinking` API with `budget_tokens` instead.
#[serde(default)]
pub effort: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]