use super::anthropic::{
claude_generation, claude_model_supports_tool_search, model_defaults_to_adaptive_thinking,
reconcile_request_body, strip_unsupported_bedrock_converse_sampling_params,
strip_unsupported_sampling_params, AnthropicProvider,
};
use super::anthropic_test_support::base_payload;
use crate::llm::api::{ReasoningEffort, ThinkingConfig};
#[test]
fn fable_and_mythos_parse_generation_and_inherit_guards() {
assert_eq!(claude_generation("claude-fable-5"), Some((5, 0)));
assert_eq!(claude_generation("claude-mythos-5"), Some((5, 0)));
assert_eq!(claude_generation("anthropic/claude-fable-5"), Some((5, 0)));
assert_eq!(
claude_generation("anthropic.claude-opus-4-7-v1:0"),
Some((4, 7))
);
assert_eq!(
claude_generation("anthropic.claude-3-5-sonnet-20240620-v1:0"),
Some((3, 5))
);
assert_eq!(claude_generation("claude-mythos-preview"), None);
assert!(claude_model_supports_tool_search("claude-fable-5"));
}
#[test]
fn fable_thinking_payloads_match_always_on_surface() {
let mut payload = base_payload();
payload.model = "claude-fable-5".to_string();
payload.thinking = ThinkingConfig::Enabled {
budget_tokens: Some(4096),
};
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(body["thinking"], serde_json::json!({ "type": "adaptive" }));
let mut payload2 = base_payload();
payload2.model = "claude-fable-5".to_string();
payload2.thinking = ThinkingConfig::Disabled;
payload2.temperature = Some(0.0);
let body2 = AnthropicProvider::build_request_body(&payload2);
assert!(body2.get("thinking").is_none());
assert!(
body2.get("temperature").is_none(),
"temperature must be stripped for claude-fable-5"
);
}
#[test]
fn opus_5_disabled_thinking_is_explicit_not_omitted() {
let mut payload = base_payload();
payload.model = "claude-opus-5".to_string();
payload.thinking = ThinkingConfig::Disabled;
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(
body["thinking"],
serde_json::json!({ "type": "disabled" }),
"Opus 5 thinks when `thinking` is omitted; Disabled must be explicit"
);
let mut prior = base_payload();
prior.model = "claude-opus-4-8".to_string();
prior.thinking = ThinkingConfig::Disabled;
assert!(AnthropicProvider::build_request_body(&prior)
.get("thinking")
.is_none());
}
#[test]
fn always_on_models_lower_thinking_off_to_the_lowest_effort() {
for model in ["claude-opus-5-5", "claude-fable-5-1"] {
let mut payload = base_payload();
payload.model = model.to_string();
payload.thinking = ThinkingConfig::Disabled;
let body = AnthropicProvider::build_request_body(&payload);
assert!(body.get("thinking").is_none(), "{model}: {body}");
assert_eq!(body["output_config"]["effort"], "low", "{model}: {body}");
let mut none = base_payload();
none.model = model.to_string();
none.thinking = ThinkingConfig::Effort {
level: crate::llm::api::ReasoningEffort::None,
};
let body = AnthropicProvider::build_request_body(&none);
assert!(body.get("thinking").is_none(), "{model}: {body}");
assert_eq!(body["output_config"]["effort"], "low", "{model}: {body}");
}
let mut opus5 = base_payload();
opus5.model = "claude-opus-5".to_string();
opus5.thinking = ThinkingConfig::Disabled;
let body = AnthropicProvider::build_request_body(&opus5);
assert_eq!(body["thinking"], serde_json::json!({ "type": "disabled" }));
assert!(body.pointer("/output_config/effort").is_none(), "{body}");
}
#[test]
fn always_on_models_relower_a_disabled_thinking_override_at_egress() {
let reconciled = |model: &str, body: serde_json::Value| {
let mut body = body;
reconcile_request_body(
&mut body,
"anthropic",
model,
&ThinkingConfig::Disabled,
None,
);
body
};
let body = reconciled(
"claude-opus-5-5",
serde_json::json!({"model": "claude-opus-5-5", "thinking": {"type": "disabled"}}),
);
assert!(body.get("thinking").is_none(), "{body}");
assert_eq!(body["output_config"]["effort"], "low", "{body}");
let body = reconciled(
"claude-opus-5-5",
serde_json::json!({
"model": "claude-opus-5-5",
"thinking": {"type": "disabled"},
"output_config": {"effort": "high"},
}),
);
assert!(body.get("thinking").is_none(), "{body}");
assert_eq!(body["output_config"]["effort"], "high", "{body}");
let body = reconciled(
"claude-opus-5",
serde_json::json!({"model": "claude-opus-5", "thinking": {"type": "disabled"}}),
);
assert_eq!(body["thinking"], serde_json::json!({ "type": "disabled" }));
}
#[test]
fn sonnet_55_lowers_thinking_off_to_between_tools() {
let built = |model: &str| {
let mut payload = base_payload();
payload.model = model.to_string();
payload.thinking = ThinkingConfig::Disabled;
AnthropicProvider::build_request_body(&payload)
};
assert_eq!(
built("claude-sonnet-5-5")["thinking"],
serde_json::json!({ "type": "between_tools" })
);
assert_eq!(
built("claude-sonnet-5")["thinking"],
serde_json::json!({ "type": "disabled" })
);
let reconciled = |model: &str, thinking: &str, effort: &str| {
let mut body = serde_json::json!({
"model": model,
"thinking": {"type": thinking},
"output_config": {"effort": effort},
});
reconcile_request_body(
&mut body,
"anthropic",
model,
&ThinkingConfig::Disabled,
None,
);
body
};
let body = reconciled("claude-sonnet-5-5", "disabled", "medium");
assert_eq!(
body["thinking"],
serde_json::json!({ "type": "between_tools" })
);
assert_eq!(body["output_config"]["effort"], "medium");
let body = reconciled("claude-sonnet-5", "between_tools", "medium");
assert_eq!(body["thinking"], serde_json::json!({ "type": "disabled" }));
let body = reconciled("claude-opus-4-8", "between_tools", "medium");
assert!(body.get("thinking").is_none(), "{body}");
for effort in ["xhigh", "max"] {
let body = reconciled("claude-sonnet-5-5", "between_tools", effort);
assert_eq!(body["output_config"]["effort"], "high", "{effort}");
assert_eq!(
body["thinking"],
serde_json::json!({ "type": "between_tools" })
);
}
}
#[test]
fn opus_5_clamps_effort_when_thinking_is_disabled() {
let reconciled = |model: &str, effort: &str| {
let mut body = serde_json::json!({
"model": model,
"thinking": {"type": "disabled"},
"output_config": {"effort": effort},
});
reconcile_request_body(
&mut body,
"anthropic",
model,
&ThinkingConfig::Disabled,
None,
);
body
};
for requested in ["xhigh", "max"] {
let body = reconciled("claude-opus-5", requested);
assert_eq!(
body["output_config"]["effort"],
serde_json::json!("high"),
"effort `{requested}` must clamp to `high` when thinking is disabled"
);
assert_eq!(body["thinking"], serde_json::json!({ "type": "disabled" }));
}
assert_eq!(
reconciled("claude-opus-5", "medium")["output_config"]["effort"],
serde_json::json!("medium")
);
let mut thinking_on = serde_json::json!({
"model": "claude-opus-5",
"output_config": {"effort": "xhigh"},
});
reconcile_request_body(
&mut thinking_on,
"anthropic",
"claude-opus-5",
&ThinkingConfig::Effort {
level: ReasoningEffort::XHigh,
},
None,
);
assert_eq!(
thinking_on["output_config"]["effort"],
serde_json::json!("xhigh"),
"effort must not be clamped while thinking is active"
);
assert_eq!(
reconciled("claude-opus-4-8", "xhigh")["output_config"]["effort"],
serde_json::json!("xhigh")
);
}
#[test]
fn generation_5_drives_default_on_thinking_not_a_model_id_list() {
for model in [
"claude-opus-5",
"claude-sonnet-5",
"claude-fable-5",
"claude-mythos-5",
"anthropic/claude-opus-5",
] {
assert!(
model_defaults_to_adaptive_thinking(model),
"{model} should default adaptive thinking on"
);
}
for model in ["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"] {
assert!(
!model_defaults_to_adaptive_thinking(model),
"{model} needs an explicit thinking field to reason"
);
}
}
#[test]
fn sonnet_5_effort_uses_output_config_and_default_on_thinking() {
let mut payload = base_payload();
payload.model = "claude-sonnet-5".to_string();
payload.thinking = ThinkingConfig::Effort {
level: ReasoningEffort::High,
};
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(body["output_config"]["effort"], serde_json::json!("high"));
assert!(
body.get("thinking").is_none(),
"Sonnet 5 defaults adaptive thinking on; effort should not send legacy thinking budgets"
);
let mut disabled = base_payload();
disabled.model = "claude-sonnet-5".to_string();
disabled.thinking = ThinkingConfig::Disabled;
let disabled_body = AnthropicProvider::build_request_body(&disabled);
assert_eq!(
disabled_body["thinking"],
serde_json::json!({ "type": "disabled" })
);
assert!(
disabled_body.get("output_config").is_none(),
"turning Sonnet 5 thinking off should not also send an effort level"
);
}
#[test]
fn opus_adaptive_effort_uses_output_config_with_adaptive_thinking() {
let mut payload = base_payload();
payload.model = "claude-opus-4-7".to_string();
payload.thinking = ThinkingConfig::Effort {
level: ReasoningEffort::Max,
};
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(body["thinking"], serde_json::json!({ "type": "adaptive" }));
assert_eq!(body["output_config"]["effort"], serde_json::json!("max"));
}
#[test]
fn temperature_stripped_when_thinking_active() {
let mut payload = base_payload();
payload.temperature = Some(0.0);
payload.thinking = ThinkingConfig::Adaptive;
let body = AnthropicProvider::build_request_body(&payload);
assert!(
body.get("temperature").is_none(),
"temperature must be stripped when thinking is active to avoid HTTP 400"
);
let mut payload2 = base_payload();
payload2.temperature = Some(0.0);
payload2.thinking = ThinkingConfig::Disabled;
let body2 = AnthropicProvider::build_request_body(&payload2);
assert_eq!(body2["temperature"], serde_json::json!(0.0));
}
#[test]
fn sampling_params_stripped_by_shared_helper_for_rejecting_models() {
let mut body = serde_json::json!({
"model": "claude-opus-4-7",
"temperature": 0.2,
"top_p": 0.9,
"top_k": 20,
});
strip_unsupported_sampling_params(
&mut body,
"claude-opus-4-7",
&ThinkingConfig::Disabled,
None,
);
assert!(body.get("temperature").is_none());
assert!(body.get("top_p").is_none());
assert!(body.get("top_k").is_none());
assert_eq!(body["model"], serde_json::json!("claude-opus-4-7"));
}
#[test]
fn option_probe_preserves_only_its_selected_sampling_field() {
use crate::llm::capabilities::PortableOption;
let mut direct = serde_json::json!({
"model": "claude-opus-4-7",
"temperature": 0.2,
"top_p": 0.9,
"top_k": 20,
});
strip_unsupported_sampling_params(
&mut direct,
"claude-opus-4-7",
&ThinkingConfig::Disabled,
Some(PortableOption::TopP),
);
assert!(direct.get("temperature").is_none());
assert_eq!(direct["top_p"], serde_json::json!(0.9));
assert!(direct.get("top_k").is_none());
let mut bedrock = serde_json::json!({
"inferenceConfig": {"temperature": 0.2, "topP": 0.9, "topK": 20},
});
strip_unsupported_bedrock_converse_sampling_params(
&mut bedrock,
"anthropic.claude-opus-4-7-v1:0",
&ThinkingConfig::Disabled,
Some(PortableOption::TopP),
);
assert!(bedrock["inferenceConfig"].get("temperature").is_none());
assert_eq!(bedrock["inferenceConfig"]["topP"], serde_json::json!(0.9));
assert!(bedrock["inferenceConfig"].get("topK").is_none());
}
#[test]
fn final_reconciliation_preserves_probe_option_and_removes_rejected_prefill() {
use crate::llm::capabilities::PortableOption;
let mut body = serde_json::json!({
"model": "claude-opus-5",
"messages": [
{"role": "user", "content": "Reply with ok"},
{"role": "assistant", "content": "persisted prefill"},
],
"temperature": 0.2,
"top_p": 0.9,
});
reconcile_request_body(
&mut body,
"anthropic",
"claude-opus-5",
&ThinkingConfig::Disabled,
Some(PortableOption::Temperature),
);
assert_eq!(body["temperature"], serde_json::json!(0.2));
assert!(body.get("top_p").is_none());
assert_eq!(
body["messages"],
serde_json::json!([{"role": "user", "content": "Reply with ok"}])
);
}
#[test]
fn sampling_params_stripped_by_shared_helper_when_thinking_active() {
let mut body = serde_json::json!({
"model": "claude-sonnet-4-6",
"temperature": 0.0,
"top_p": 0.9,
"top_k": 20,
});
strip_unsupported_sampling_params(
&mut body,
"claude-sonnet-4-6",
&ThinkingConfig::Adaptive,
None,
);
assert!(body.get("temperature").is_none());
assert!(body.get("top_p").is_none());
assert!(body.get("top_k").is_none());
}
#[test]
fn sampling_params_preserved_by_shared_helper_for_supported_disabled_thinking() {
let mut body = serde_json::json!({
"model": "claude-sonnet-4-6",
"temperature": 0.2,
"top_p": 0.9,
"top_k": 20,
});
strip_unsupported_sampling_params(
&mut body,
"claude-sonnet-4-6",
&ThinkingConfig::Disabled,
None,
);
assert_eq!(body["temperature"], serde_json::json!(0.2));
assert_eq!(body["top_p"], serde_json::json!(0.9));
assert_eq!(body["top_k"], serde_json::json!(20));
}
#[test]
fn sampling_params_stripped_when_body_thinking_override_is_active() {
let mut body = serde_json::json!({
"model": "claude-sonnet-4-6",
"temperature": 0.2,
"top_p": 0.9,
"thinking": {"type": "enabled", "budget_tokens": 1024},
});
strip_unsupported_sampling_params(
&mut body,
"claude-sonnet-4-6",
&ThinkingConfig::Disabled,
None,
);
assert!(body.get("temperature").is_none());
assert!(body.get("top_p").is_none());
assert_eq!(
body["thinking"],
serde_json::json!({"type": "enabled", "budget_tokens": 1024})
);
}
#[test]
fn sampling_params_stripped_when_body_output_config_effort_override_is_active() {
let mut body = serde_json::json!({
"model": "claude-sonnet-4-6",
"temperature": 0.2,
"top_p": 0.9,
"output_config": {"effort": "high"},
});
strip_unsupported_sampling_params(
&mut body,
"claude-sonnet-4-6",
&ThinkingConfig::Disabled,
None,
);
assert!(body.get("temperature").is_none());
assert!(body.get("top_p").is_none());
assert_eq!(body["output_config"], serde_json::json!({"effort": "high"}));
}
#[test]
fn an_unnamed_thinking_budget_stays_under_the_output_cap() {
for (max_tokens, expected) in [(2048, 2047), (8192, 8191), (64_000, 10_000), (512, 1024)] {
let mut payload = base_payload();
payload.model = "claude-sonnet-4-6".to_string();
payload.max_tokens = max_tokens;
payload.thinking = ThinkingConfig::Enabled {
budget_tokens: None,
};
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(
body["thinking"],
serde_json::json!({"type": "enabled", "budget_tokens": expected}),
"max_tokens {max_tokens}"
);
}
let mut payload = base_payload();
payload.model = "claude-sonnet-4-6".to_string();
payload.max_tokens = 2048;
payload.thinking = ThinkingConfig::Enabled {
budget_tokens: Some(1500),
};
let body = AnthropicProvider::build_request_body(&payload);
assert_eq!(body["thinking"]["budget_tokens"], 1500);
}