use serde_json::json;
use crate::catalog::ModelSpec;
use crate::completion::options::{Mapping, OptionFields, OptionMap};
use crate::completion::{CacheRetention, CompletionRequest, Effort, Reasoning, ServiceTier};
use crate::message::ToolChoice;
use super::wire::{ANTHROPIC, MINIMAX, MOONSHOT, Messages, XIAOMIMIMO, ZAI};
fn claude_reasoning(
spec: Option<&ModelSpec>,
reasoning: &Reasoning,
max_tokens: Option<u64>,
) -> Mapping {
let support = spec.map(|spec| &spec.reasoning);
if let Some(support) = support.filter(|support| !support.supported()) {
return match support.refusal(reasoning) {
Some(reason) => Mapping::unsupported(reason),
None => Mapping::Omit("the model does not reason"),
};
}
match reasoning {
Reasoning::Off => match spec {
Some(spec) if spec.reasoning.can_disable() == Some(false) => {
Mapping::unsupported("thinking cannot be disabled on this model")
}
_ => {
let off = spec
.and_then(|spec| spec.compat.thinking_off.as_deref())
.unwrap_or("disabled");
Mapping::Send(json!({"thinking": {"type": off}}))
}
},
Reasoning::Effort(Effort::Minimal) => {
Mapping::unsupported("Claude has no `minimal` effort level")
}
Reasoning::Effort(effort) => match spec {
Some(spec) if spec.reasoning.levels().is_some_and(<[Effort]>::is_empty) => {
Mapping::unsupported("this model takes a thinking budget, not an effort level")
}
Some(spec)
if spec
.reasoning
.levels()
.is_some_and(|levels| !levels.contains(effort)) =>
{
Mapping::unsupported(format!(
"this model has no `{}` effort level",
effort.as_str()
))
}
Some(spec) if !spec.compat.adaptive_thinking => {
Mapping::Send(json!({"output_config": {"effort": effort.as_str()}}))
}
_ => Mapping::Send(json!({
"thinking": {"type": "adaptive"},
"output_config": {"effort": effort.as_str()},
})),
},
Reasoning::Budget { tokens } => match support {
Some(support) if support.levels().is_some() && support.budget().is_none() => {
Mapping::unsupported("this model takes an effort level, not a thinking budget")
}
_ if *tokens < 1024 => {
Mapping::unsupported("a thinking budget must be at least 1024 tokens")
}
_ if max_tokens.is_some_and(|cap| u64::from(*tokens) >= cap) => {
Mapping::unsupported("a thinking budget must be below `max_tokens`")
}
_ => Mapping::Send(json!({
"thinking": {"type": "enabled", "budget_tokens": tokens},
})),
},
}
}
fn claude_top_p(
spec: Option<&ModelSpec>,
reasoning: Option<&Reasoning>,
top_p: f64,
temperature: bool,
) -> Mapping {
if let Some(reason) =
spec.and_then(|spec| spec.sampling_refusal("top_p", spec.reasons_with(reasoning)))
{
return Mapping::unsupported(reason);
}
match temperature {
true => Mapping::unsupported("this model takes `temperature` or `top_p`, not both"),
false => Mapping::Send(json!({"top_p": top_p})),
}
}
fn parallel_tool_calls(request: &CompletionRequest, has_tools: bool, parallel: bool) -> Mapping {
if parallel {
return Mapping::Omit("parallel tool use is the default");
}
if !has_tools {
return Mapping::Omit("the request declares no tools to call in parallel");
}
match &request.tool_choice {
None | Some(ToolChoice::Auto) => Mapping::Send(json!({
"tool_choice": {"type": "auto", "disable_parallel_tool_use": true},
})),
Some(ToolChoice::Required | ToolChoice::Specific { .. }) => Mapping::Send(json!({
"tool_choice": {"disable_parallel_tool_use": true},
})),
Some(ToolChoice::None) => Mapping::Omit("the request calls no tools"),
}
}
fn stop_sequences(stop: &[String]) -> Mapping {
Mapping::Send(json!({"stop_sequences": stop}))
}
pub(super) fn map_options(
wire: &Messages,
request: &CompletionRequest,
fields: OptionFields<'_>,
) -> OptionMap {
let model = request.model.as_deref().unwrap_or(&wire.model);
let max_tokens = request.max_tokens.or_else(|| {
if model == wire.model {
wire.default_max_tokens
} else {
wire.provider.dialect.default_max_tokens(&wire.facts, model)
}
});
let has_tools = crate::completion::history::declares_tools(request);
match wire.provider.dialect.name {
name if name == ANTHROPIC.name => {
anthropic(wire, request, model, max_tokens, has_tools, fields)
}
name if name == MINIMAX.name => minimax(model, fields),
name if name == XIAOMIMIMO.name => xiaomimimo(request, has_tools, fields),
name if name == ZAI.name || name == MOONSHOT.name => implicit_cache_gateway(fields),
_ => unknown(fields),
}
}
fn anthropic(
wire: &Messages,
request: &CompletionRequest,
model: &str,
max_tokens: Option<u64>,
has_tools: bool,
fields: OptionFields<'_>,
) -> OptionMap {
let spec = super::completion::spec(&wire.facts, model);
let places = wire.prompt_caching || wire.static_prefix_cache_ttl.is_some();
OptionMap {
reasoning: Mapping::of(fields.reasoning, |reasoning| {
claude_reasoning(spec, reasoning, max_tokens)
}),
cache: Mapping::of(fields.cache, |cache| match cache {
CacheRetention::None if places => Mapping::unsupported(
"the wire places cache markers (`with_prompt_caching` or \
`with_static_prefix_cache_ttl`)",
),
CacheRetention::None => Mapping::Omit("no cache marker is sent"),
CacheRetention::Short => Mapping::Send(json!({"cache_control": {"type": "ephemeral"}})),
CacheRetention::Long => {
Mapping::Send(json!({"cache_control": {"type": "ephemeral", "ttl": "1h"}}))
}
}),
service_tier: Mapping::of(fields.service_tier, |tier| match tier {
ServiceTier::Auto => Mapping::Send(json!({"service_tier": "auto"})),
ServiceTier::Default => Mapping::Send(json!({"service_tier": "standard_only"})),
ServiceTier::Priority => Mapping::unsupported(
"Anthropic has no priority-only tier; `auto` uses Priority capacity under a \
commitment",
),
ServiceTier::Flex => Mapping::unsupported("Anthropic has no flex tier"),
}),
verbosity: Mapping::of(fields.verbosity, |_| {
Mapping::unsupported("Anthropic has no verbosity setting")
}),
parallel_tool_calls: Mapping::of(fields.parallel_tool_calls, |parallel| {
parallel_tool_calls(request, has_tools, parallel)
}),
top_p: Mapping::of(fields.top_p, |top_p| {
claude_top_p(spec, fields.reasoning, top_p, request.temperature.is_some())
}),
seed: Mapping::of(fields.seed, |_| {
Mapping::unsupported("Anthropic has no seed parameter")
}),
stop: Mapping::of_stop(fields.stop, stop_sequences),
cache_key: Mapping::unrouted(fields.cache_key),
}
}
fn minimax(model: &str, fields: OptionFields<'_>) -> OptionMap {
let model = model.to_ascii_lowercase();
let flash = model.starts_with("minimax-m3.1");
let m3 = !flash && model.starts_with("minimax-m3");
OptionMap {
reasoning: Mapping::of(fields.reasoning, |reasoning| match reasoning {
Reasoning::Off if m3 => Mapping::Send(json!({"thinking": {"type": "disabled"}})),
Reasoning::Off => Mapping::unsupported("thinking cannot be disabled on this model"),
Reasoning::Effort(Effort::Minimal) => {
Mapping::unsupported("MiniMax has no `minimal` effort level")
}
Reasoning::Effort(effort) if flash => {
Mapping::Send(json!({"output_config": {"effort": effort.as_str()}}))
}
Reasoning::Effort(_) => Mapping::unsupported("this model takes no effort level"),
Reasoning::Budget { .. } => Mapping::unsupported("unverified for minimax"),
}),
cache: Mapping::of(fields.cache, |cache| match cache {
CacheRetention::None => {
Mapping::Omit("passive caching cannot be turned off; no marker is sent")
}
CacheRetention::Short => Mapping::Place,
CacheRetention::Long => Mapping::unsupported("MiniMax caches for five minutes only"),
}),
service_tier: Mapping::of(fields.service_tier, |tier| match tier {
ServiceTier::Default => Mapping::Send(json!({"service_tier": "standard"})),
ServiceTier::Priority => Mapping::Send(json!({"service_tier": "priority"})),
ServiceTier::Auto | ServiceTier::Flex => {
Mapping::unsupported("MiniMax takes only the standard and priority tiers")
}
}),
verbosity: Mapping::of(fields.verbosity, |_| {
Mapping::unsupported("MiniMax has no verbosity setting")
}),
parallel_tool_calls: Mapping::of(fields.parallel_tool_calls, |parallel| match parallel {
true => Mapping::Omit("parallel tool use is the default"),
false => Mapping::unsupported("unverified for minimax"),
}),
top_p: Mapping::of(fields.top_p, |top_p| Mapping::Send(json!({"top_p": top_p}))),
seed: Mapping::of(fields.seed, |_| {
Mapping::unsupported("MiniMax has no seed parameter")
}),
stop: Mapping::of_stop(fields.stop, |_| {
Mapping::unsupported("MiniMax documents `stop_sequences` as ignored")
}),
cache_key: Mapping::unrouted(fields.cache_key),
}
}
fn xiaomimimo(request: &CompletionRequest, has_tools: bool, fields: OptionFields<'_>) -> OptionMap {
OptionMap {
reasoning: Mapping::of(fields.reasoning, |reasoning| match reasoning {
Reasoning::Off => Mapping::Send(json!({"thinking": {"type": "disabled"}})),
_ => Mapping::unsupported("MiMo takes only thinking enabled or disabled"),
}),
cache: Mapping::of(fields.cache, implicit_cache),
service_tier: Mapping::of(fields.service_tier, |_| {
Mapping::unsupported("unverified for xiaomimimo")
}),
verbosity: Mapping::of(fields.verbosity, |_| {
Mapping::unsupported("unverified for xiaomimimo")
}),
parallel_tool_calls: Mapping::of(fields.parallel_tool_calls, |parallel| {
parallel_tool_calls(request, has_tools, parallel)
}),
top_p: Mapping::of(fields.top_p, |top_p| {
if (0.01..=1.0).contains(&top_p) {
Mapping::Send(json!({"top_p": top_p}))
} else {
Mapping::unsupported("MiMo takes `top_p` from 0.01 to 1.0")
}
}),
seed: Mapping::of(fields.seed, |_| {
Mapping::unsupported("unverified for xiaomimimo")
}),
stop: Mapping::of_stop(fields.stop, stop_sequences),
cache_key: Mapping::unrouted(fields.cache_key),
}
}
fn implicit_cache(cache: &CacheRetention) -> Mapping {
match cache {
CacheRetention::None => {
Mapping::Omit("caching is implicit and cannot be turned off; no marker is sent")
}
CacheRetention::Short => Mapping::Omit("caching is implicit"),
CacheRetention::Long => Mapping::unsupported("the provider has no retention control"),
}
}
fn implicit_cache_gateway(fields: OptionFields<'_>) -> OptionMap {
const UNVERIFIED: &str = "unverified for this Messages-format provider";
OptionMap {
reasoning: Mapping::of(fields.reasoning, |_| Mapping::unsupported(UNVERIFIED)),
cache: Mapping::of(fields.cache, implicit_cache),
service_tier: Mapping::of(fields.service_tier, |_| Mapping::unsupported(UNVERIFIED)),
verbosity: Mapping::of(fields.verbosity, |_| Mapping::unsupported(UNVERIFIED)),
parallel_tool_calls: Mapping::of(fields.parallel_tool_calls, |parallel| match parallel {
true => Mapping::Omit("parallel tool use is the default"),
false => Mapping::unsupported(UNVERIFIED),
}),
top_p: Mapping::of(fields.top_p, |_| Mapping::unsupported(UNVERIFIED)),
seed: Mapping::of(fields.seed, |_| Mapping::unsupported(UNVERIFIED)),
stop: Mapping::of_stop(fields.stop, |_| Mapping::unsupported(UNVERIFIED)),
cache_key: Mapping::unrouted(fields.cache_key),
}
}
fn unknown(fields: OptionFields<'_>) -> OptionMap {
crate::providers::internal::refuse_all(
fields,
"no option mapping is known for this Messages-format provider",
)
}
#[cfg(test)]
mod tests;