use axum::response::Response;
use std::sync::Arc;
use crate::AppState;
use crate::ir::ChatRequest;
use super::{media_limits, msg_entry, template, thinking};
pub(crate) struct PreparedChat {
pub(crate) tools_active: bool,
pub(crate) cwd_hint: Option<String>,
pub(crate) image_pixels: Vec<metrale_model_layers::VisionItem>,
pub(crate) prompt_tokens: Vec<u32>,
pub(crate) enable_thinking: bool,
pub(crate) thinking_budget: Option<u32>,
}
#[allow(clippy::result_large_err)]
pub(crate) fn prepare_chat_prompt(
state: &Arc<AppState>,
req: &mut ChatRequest,
) -> Result<PreparedChat, Response> {
let tools_active = state.tool_call_parser.is_some()
&& !req.tools.is_empty()
&& !req.tool_choice.as_ref().is_some_and(|tc| tc.is_none());
if tools_active && let Some(ref parser) = state.tool_call_parser {
let default_choice = crate::tool_parser::ToolChoice::Mode("auto".to_string());
let tool_choice = req.tool_choice.as_ref().unwrap_or(&default_choice);
let tool_prompt = parser_tool_prompt(
parser.as_ref(),
&req.tools,
tool_choice,
&state.chat.prompt,
state.tokenizer.uses_native_qwen_tool_template(),
);
inject_tool_system_prompt(&mut req.messages, tool_prompt);
}
tracing::info!(
"Request: model={}, messages={}, tools={}, tools_active={}, tool_choice={:?}, stream={}, temp={:?}, max_tokens={}, freq_pen={:?}, rep_pen={:?}",
req.model,
req.messages.len(),
req.tools.len(),
tools_active,
req.tool_choice,
req.stream,
req.sampling.temperature,
req.max_tokens,
req.sampling.frequency_penalty,
req.sampling.repetition_penalty,
);
let _t_phase = std::time::Instant::now();
if let Err(over) = media_limits::check_media_limits(&req.messages, state.media_limits) {
return Err(crate::api::compact::openai_error_response_with_param(
axum::http::StatusCode::BAD_REQUEST,
over.message,
Some(over.param),
None,
));
}
let msg_entry::BuildOut {
messages,
cwd_hint,
image_pixels,
image_pad_counts,
} = msg_entry::build_msg_entries(
state.vision_config.as_ref(),
state.vision_max_pixels,
&state.remote_image_policy,
&msg_entry::VideoDecode {
ffmpeg: &state.video_ffmpeg,
fps: state.video_fps,
},
&req.messages,
tools_active,
&state.chat,
state.tokenizer.uses_deepseek_v4_encoding(),
)?;
let us_msg_entry = _t_phase.elapsed().as_micros();
let mut thinking_directive = req.thinking;
if !thinking_directive.is_explicit() {
thinking_directive = state.default_thinking;
}
let gen_max =
thinking::generation_max_tokens(req.max_tokens, tools_active, state.tool_max_tokens);
let (enable_thinking, thinking_budget) =
thinking::resolve_thinking(state, thinking_directive, gen_max as u32, tools_active);
let reasoning_effort = effective_reasoning_effort(
req.reasoning_effort,
state.default_reasoning_effort,
enable_thinking,
);
let us_thinking = _t_phase.elapsed().as_micros() - us_msg_entry;
let template::TemplateOut {
prompt_tokens,
enable_thinking,
thinking_budget,
} = template::render_template(
state,
&req.tools,
&messages,
&image_pad_counts,
enable_thinking,
thinking_budget,
reasoning_effort,
crate::tokenizer::ThinkingVars {
preserve_thinking: req.preserve_thinking.or(state.behavior.preserve_thinking),
thinking: req.template_thinking,
},
tools_active,
)?;
if state.chat.phase_timing {
let us_template = _t_phase.elapsed().as_micros() - us_msg_entry - us_thinking;
tracing::info!(
"CHAT_PHASE prepare: msg_entry={us_msg_entry}us thinking={us_thinking}us \
template_render_and_tokenize={us_template}us prompt_tokens={}",
prompt_tokens.len()
);
}
Ok(PreparedChat {
tools_active,
cwd_hint,
image_pixels,
prompt_tokens,
enable_thinking,
thinking_budget,
})
}
fn effective_reasoning_effort(
request: Option<crate::ir::ReasoningEffort>,
server_default: Option<crate::ir::ReasoningEffort>,
enable_thinking: bool,
) -> Option<crate::ir::ReasoningEffort> {
if enable_thinking {
request.or(server_default)
} else {
None
}
}
pub(crate) fn parser_tool_prompt(
parser: &dyn crate::tool_parser::ToolCallParser,
tools: &[crate::tool_parser::ToolDefinition],
choice: &crate::tool_parser::ToolChoice,
levers: &crate::tool_parser::PromptLevers,
native_qwen_template: bool,
) -> String {
if native_qwen_template && !levers.tscg && matches!(parser.name(), "qwen3_coder" | "qwen3_xml")
{
let mut prompt = String::new();
crate::tool_parser::append_tool_choice_instruction(&mut prompt, choice);
return prompt;
}
parser.system_prompt(tools, choice, levers)
}
pub(crate) fn inject_tool_system_prompt(
messages: &mut Vec<crate::ir::Message>,
tool_prompt: String,
) {
if tool_prompt.is_empty() {
return;
}
if let Some(first) = messages
.first_mut()
.filter(|m| m.role == crate::ir::Role::System)
{
first.prepend_text(&format!("{tool_prompt}\n\n"));
} else {
messages.insert(0, crate::ir::Message::synthetic_system(tool_prompt));
}
}
#[cfg(test)]
mod tests {
use super::{effective_reasoning_effort, inject_tool_system_prompt};
use crate::ir::{Message, ReasoningEffort, Role};
#[test]
fn effort_resolution_contract() {
let low = Some(ReasoningEffort::Low);
let max = Some(ReasoningEffort::Max);
assert_eq!(effective_reasoning_effort(low, max, true), low);
assert_eq!(effective_reasoning_effort(None, max, true), max);
assert_eq!(effective_reasoning_effort(None, None, true), None);
assert_eq!(effective_reasoning_effort(low, max, false), None);
assert_eq!(effective_reasoning_effort(None, max, false), None);
}
#[test]
fn empty_tool_prompt_does_not_change_existing_system_message() {
let mut messages = vec![Message::synthetic_system("original".into())];
let before = messages.clone();
inject_tool_system_prompt(&mut messages, String::new());
assert_eq!(messages, before);
}
#[test]
fn empty_tool_prompt_does_not_insert_system_message() {
let mut messages = vec![Message {
role: Role::User,
content: vec![crate::ir::ContentPart::Text("hello".into())],
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
reasoning: None,
tool_error: false,
}];
let before = messages.clone();
inject_tool_system_prompt(&mut messages, String::new());
assert_eq!(messages, before);
}
#[test]
fn nonempty_tool_prompt_preserves_existing_injection_behavior() {
let mut messages = vec![Message::synthetic_system("original".into())];
inject_tool_system_prompt(&mut messages, "tool instructions".into());
assert_eq!(messages[0].text(), "tool instructions\n\noriginal");
}
}
#[cfg(test)]
#[path = "../../tokenizer/tests/native_tool_prompt.rs"]
mod native_tool_prompt;