use rig_core::{completion::CompletionRequest, message::ToolChoice};
use serde::Deserialize;
use crate::{
client::Defaults,
engine::{generate::GenerateOptions, tools::Tool},
error::Error,
};
#[derive(Debug, Default, Deserialize)]
struct ExtraParams {
temperature: Option<f32>,
max_tokens: Option<usize>,
top_p: Option<f32>,
top_k: Option<i32>,
seed: Option<u64>,
enable_thinking: Option<bool>,
reasoning_budget_tokens: Option<usize>,
prompt_cache: Option<bool>,
speculative_tokens: Option<usize>,
}
const MAX_TOKENS_CEILING: usize = 1 << 20;
pub(super) fn generate_options(
request: &CompletionRequest,
defaults: &Defaults,
) -> Result<GenerateOptions, Error> {
let extra = match &request.additional_params {
Some(value) => {
if !crate::json::structurally_bounded(value) {
return Err(Error::InvalidRequest(
"additional_params exceeds JSON structural limits".to_string(),
));
}
ExtraParams::deserialize(value)?
}
None => ExtraParams::default(),
};
let mut sampling = defaults.sampling;
if let Some(temperature) = request.temperature {
sampling.temperature = temperature as f32;
}
if let Some(temperature) = extra.temperature {
sampling.temperature = temperature;
}
if let Some(top_p) = extra.top_p {
sampling.top_p = top_p;
}
if let Some(top_k) = extra.top_k {
sampling.top_k = (top_k > 0).then_some(top_k);
}
if let Some(seed) = extra.seed {
sampling.seed = Some(seed);
}
let requested_max_tokens = extra
.max_tokens
.or_else(|| request.max_tokens.and_then(|max| usize::try_from(max).ok()))
.unwrap_or(defaults.max_tokens);
let requested_speculative_tokens = extra.speculative_tokens.or(defaults.speculative_tokens);
let enable_thinking = extra.enable_thinking.or(defaults.enable_thinking);
let reasoning_budget_tokens = match (extra.reasoning_budget_tokens, extra.enable_thinking) {
(Some(budget), _) => Some(budget),
(None, Some(false)) => None,
(None, _) => defaults.reasoning_budget_tokens,
};
let resolved = GenerateOptions {
max_tokens: requested_max_tokens,
context_tokens: defaults.context_tokens,
sampling,
enable_thinking,
reasoning_budget_tokens,
prompt_cache: extra.prompt_cache.or(defaults.prompt_cache),
speculative_tokens: requested_speculative_tokens.filter(|&tokens| tokens > 0),
};
if resolved.max_tokens == 0 {
return Err(Error::InvalidRequest(
"max_tokens must be positive".to_string(),
));
}
if resolved.max_tokens > MAX_TOKENS_CEILING {
return Err(Error::InvalidRequest(format!(
"max_tokens must be at most {MAX_TOKENS_CEILING}"
)));
}
if !resolved.sampling.temperature.is_finite()
|| !(0.0..=2.0).contains(&resolved.sampling.temperature)
{
return Err(Error::InvalidRequest(
"temperature must be finite and in 0..=2".to_string(),
));
}
if !resolved.sampling.top_p.is_finite() || !(0.0..=1.0).contains(&resolved.sampling.top_p) {
return Err(Error::InvalidRequest(
"top_p must be finite and in 0..=1".to_string(),
));
}
if requested_speculative_tokens
.is_some_and(|tokens| tokens > crate::engine::generate::SPECULATIVE_TOKENS_CEILING)
{
return Err(Error::InvalidRequest(format!(
"speculative_tokens must be at most {}",
crate::engine::generate::SPECULATIVE_TOKENS_CEILING
)));
}
if resolved
.reasoning_budget_tokens
.is_some_and(|budget| budget == 0 || budget > resolved.max_tokens)
{
return Err(Error::InvalidRequest(
"reasoning_budget_tokens must be positive and not exceed max_tokens".to_string(),
));
}
if resolved.reasoning_budget_tokens.is_some() && resolved.enable_thinking != Some(true) {
return Err(Error::InvalidRequest(
"reasoning_budget_tokens requires thinking to be enabled".to_string(),
));
}
Ok(resolved)
}
pub(super) fn tools_and_instruction(
request: &CompletionRequest,
) -> Result<(Option<Vec<Tool>>, Option<String>), Error> {
let tools: Vec<Tool> = request
.tools
.iter()
.map(|tool| {
Tool::new(
tool.name.clone(),
tool.description.clone(),
tool.parameters.clone(),
)
})
.collect();
if tools.is_empty() {
return match &request.tool_choice {
Some(ToolChoice::Required) => Err(Error::InvalidRequest(
"tool_choice=Required requires at least one available tool".to_string(),
)),
Some(ToolChoice::Specific { function_names }) if function_names.is_empty() => {
Err(Error::InvalidRequest(
"specific tool choice requires at least one function name".to_string(),
))
}
Some(ToolChoice::Specific { function_names }) => Err(Error::InvalidRequest(format!(
"specific tool choice names unavailable functions: {}",
function_names.join(", ")
))),
None | Some(ToolChoice::Auto | ToolChoice::None) => Ok((None, None)),
};
}
let resolved = match &request.tool_choice {
None | Some(ToolChoice::Auto) => (Some(tools), None),
Some(ToolChoice::None) => (None, None),
Some(ToolChoice::Required) => {
tracing::warn!(
"tool_choice=Required is best-effort: the local engine cannot force \
tool-call decoding"
);
(
Some(tools),
Some(
"You MUST respond by calling one of the available tools; do not \
answer in plain text."
.to_string(),
),
)
}
Some(ToolChoice::Specific { function_names }) => {
if function_names.is_empty() {
return Err(Error::InvalidRequest(
"specific tool choice requires at least one function name".to_string(),
));
}
tracing::warn!(
"tool_choice=Specific is best-effort: the local engine cannot force \
tool-call decoding"
);
let named: Vec<Tool> = tools
.iter()
.filter(|tool| function_names.contains(&tool.function.name))
.cloned()
.collect();
let unknown = function_names
.iter()
.filter(|name| !tools.iter().any(|tool| tool.function.name == name.as_str()))
.cloned()
.collect::<Vec<_>>();
if !unknown.is_empty() {
return Err(Error::InvalidRequest(format!(
"specific tool choice names unavailable functions: {}",
unknown.join(", ")
)));
}
(
Some(named),
Some(format!(
"You MUST respond by calling one of these tools: {}; do not answer \
in plain text.",
function_names.join(", ")
)),
)
}
};
Ok(resolved)
}
pub(super) fn schema_instruction(request: &CompletionRequest) -> Result<Option<String>, Error> {
let Some(schema) = &request.output_schema else {
return Ok(None);
};
let encoded = super::bounded_json_bytes(schema.as_value(), 1 << 20, "output schema")?;
let schema_json = String::from_utf8(encoded)
.map_err(|_| Error::InvalidRequest("output schema is not valid UTF-8 JSON".to_string()))?;
Ok(Some(format!(
"Respond ONLY with a JSON object that conforms to this JSON Schema, with \
no surrounding prose or code fences:\n{schema_json}"
)))
}