use locode_protocol::{ContentBlock, ImageSource, ResultChunk, Role};
use super::config::{DeveloperRendering, ModelConfig, ReasoningEncoding};
use super::wire;
pub use crate::http::normalize_input_schema;
use crate::request::{CacheHint, ConversationRequest, ReasoningEffort};
#[must_use]
pub fn build_request(req: &ConversationRequest, cfg: &ModelConfig) -> wire::MessagesRequest {
let (mut system_blocks, mut messages) = map_messages(req, cfg);
if req.cache_hint == CacheHint::Standard {
if let Some(last) = system_blocks.last_mut() {
last.cache_control = Some(wire::CacheControl::ephemeral());
}
if let Some(last_message) = messages.last_mut() {
mark_last_cache_capable(last_message);
}
}
let system = match system_blocks.len() {
0 => None,
1 if system_blocks[0].cache_control.is_none() => {
Some(wire::SystemParam::Text(system_blocks.remove(0).text))
}
_ => Some(wire::SystemParam::Blocks(system_blocks)),
};
let (thinking, output_config) = map_reasoning(req, cfg);
let thinking_on = matches!(
thinking,
Some(wire::ThinkingConfig::Enabled { .. } | wire::ThinkingConfig::Adaptive { .. })
);
let temperature = if thinking_on {
None
} else {
req.sampling_args.temperature
};
let tools: Vec<wire::ToolParam> = req
.tools
.iter()
.map(|spec| wire::ToolParam {
name: spec.name.clone(),
description: Some(spec.description.clone()),
input_schema: match &spec.input {
locode_protocol::ToolInputFormat::JsonSchema { parameters } => {
normalize_input_schema(parameters.clone())
}
locode_protocol::ToolInputFormat::Freeform { .. } => serde_json::json!({
"type": "object",
"properties": {"input": {"type": "string",
"description": "The entire raw text input."}},
"required": ["input"],
"additionalProperties": false
}),
},
})
.collect();
let built = wire::MessagesRequest {
model: cfg.model.clone(),
messages,
max_tokens: req.sampling_args.max_tokens.min(cfg.max_tokens_cap),
system,
tools: (!tools.is_empty()).then_some(tools),
tool_choice: None,
temperature,
top_p: req.sampling_args.top_p,
top_k: None,
stream: Some(false),
stop_sequences: None,
thinking,
output_config,
metadata: None,
provider: cfg.effective_provider_prefs(),
};
let markers = count_cache_controls(&built);
assert!(
markers <= 4,
"cache_control marker count {markers} exceeds Anthropic's hard cap of 4"
);
built
}
fn map_messages(
req: &ConversationRequest,
cfg: &ModelConfig,
) -> (Vec<wire::TextBlock>, Vec<wire::Message>) {
let mut system_blocks: Vec<wire::TextBlock> = Vec::new();
let mut messages: Vec<wire::Message> = Vec::new();
for message in &req.messages {
match message.role {
Role::System => {
for block in &message.content {
if let ContentBlock::Text { text } = block {
system_blocks.push(wire::TextBlock {
r#type: "text".to_string(),
text: text.clone(),
cache_control: None,
});
}
}
}
Role::Developer => {
let text = concat_text(&message.content);
if !text.is_empty() {
messages.push(match cfg.developer_rendering {
DeveloperRendering::SystemReminder => wire::Message {
role: wire::MessageRole::User,
content: wire::MessageContent::Text(format!(
"<system-reminder>\n{text}\n</system-reminder>"
)),
},
DeveloperRendering::MidConversationSystemBeta => wire::Message {
role: wire::MessageRole::System,
content: wire::MessageContent::Text(text),
},
});
}
}
Role::User => {
let blocks = map_user_blocks(&message.content);
if !blocks.is_empty() {
messages.push(wire::Message {
role: wire::MessageRole::User,
content: wire::MessageContent::Blocks(blocks),
});
}
}
Role::Assistant => {
let blocks = map_assistant_blocks(&message.content);
if !blocks.is_empty() {
messages.push(wire::Message {
role: wire::MessageRole::Assistant,
content: wire::MessageContent::Blocks(blocks),
});
}
}
}
}
(system_blocks, messages)
}
fn concat_text(blocks: &[ContentBlock]) -> String {
let mut out = String::new();
for block in blocks {
if let ContentBlock::Text { text } = block {
if !out.is_empty() {
out.push('\n');
}
out.push_str(text);
}
}
out
}
fn map_user_blocks(blocks: &[ContentBlock]) -> Vec<wire::ContentBlock> {
let mut out = Vec::with_capacity(blocks.len());
for block in blocks {
match block {
ContentBlock::Text { text } => out.push(wire::ContentBlock::Text {
text: text.clone(),
cache_control: None,
}),
ContentBlock::Image { source } => out.push(wire::ContentBlock::Image {
source: map_image_source(source),
}),
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => out.push(wire::ContentBlock::ToolResult {
tool_use_id: tool_use_id.clone(),
content: map_tool_result_content(content),
is_error: *is_error,
cache_control: None,
}),
_ => {}
}
}
out
}
fn map_assistant_blocks(blocks: &[ContentBlock]) -> Vec<wire::ContentBlock> {
let mut out = Vec::with_capacity(blocks.len());
for block in blocks {
match block {
ContentBlock::Text { text } => out.push(wire::ContentBlock::Text {
text: text.clone(),
cache_control: None,
}),
ContentBlock::Reasoning {
format: locode_protocol::ReasoningFormat::Anthropic,
text,
signature: Some(signature),
..
} => {
if !text.is_empty() || !signature.is_empty() {
out.push(wire::ContentBlock::Thinking {
thinking: text.clone(),
signature: signature.clone(),
});
}
}
ContentBlock::Reasoning {
format: locode_protocol::ReasoningFormat::AnthropicRedacted,
payload: Some(payload),
..
} => {
if let Some(data) = payload.as_str() {
out.push(wire::ContentBlock::RedactedThinking {
data: data.to_owned(),
});
}
}
ContentBlock::ToolUse { id, name, input } => {
out.push(wire::ContentBlock::ToolUse {
id: id.clone(),
name: name.clone(),
input: input.clone(),
});
}
_ => {}
}
}
out
}
fn map_tool_result_content(chunks: &[ResultChunk]) -> wire::ToolResultContent {
if let [ResultChunk::Text { text }] = chunks {
return wire::ToolResultContent::Text(text.clone());
}
let blocks = chunks
.iter()
.map(|chunk| match chunk {
ResultChunk::Text { text } => wire::ContentBlock::Text {
text: text.clone(),
cache_control: None,
},
ResultChunk::Image { source } => wire::ContentBlock::Image {
source: map_image_source(source),
},
})
.collect();
wire::ToolResultContent::Blocks(blocks)
}
fn map_image_source(source: &ImageSource) -> wire::ImageSource {
match source {
ImageSource::Base64 { media_type, data } => wire::ImageSource::Base64 {
media_type: media_type.clone(),
data: data.clone(),
},
ImageSource::Url { url } => wire::ImageSource::Url { url: url.clone() },
}
}
fn mark_last_cache_capable(message: &mut wire::Message) {
match &mut message.content {
wire::MessageContent::Text(_) => {}
wire::MessageContent::Blocks(blocks) => {
for block in blocks.iter_mut().rev() {
match block {
wire::ContentBlock::Text { cache_control, .. }
| wire::ContentBlock::ToolResult { cache_control, .. } => {
*cache_control = Some(wire::CacheControl::ephemeral());
return;
}
_ => {}
}
}
}
}
}
fn map_reasoning(
req: &ConversationRequest,
cfg: &ModelConfig,
) -> (Option<wire::ThinkingConfig>, Option<wire::OutputConfig>) {
let Some(effort) = req.sampling_args.reasoning_effort.as_ref() else {
return (None, None);
};
match cfg.reasoning_encoding {
ReasoningEncoding::Budget => {
let budget = match effort {
ReasoningEffort::None | ReasoningEffort::Minimal | ReasoningEffort::Other(_) => {
return (None, None);
}
ReasoningEffort::Low => 4096,
ReasoningEffort::Medium => 8192,
ReasoningEffort::High => 16384,
ReasoningEffort::XHigh => 32768,
};
let budget = if cfg.interleaved_thinking() {
budget
} else {
budget.min(req.sampling_args.max_tokens.min(cfg.max_tokens_cap) - 1)
};
(
Some(wire::ThinkingConfig::Enabled {
budget_tokens: budget,
}),
None,
)
}
ReasoningEncoding::EffortAdaptive => {
let effort_str = match effort {
ReasoningEffort::None | ReasoningEffort::Minimal => return (None, None),
ReasoningEffort::Low => "low",
ReasoningEffort::Medium => "medium",
ReasoningEffort::High => "high",
ReasoningEffort::XHigh => "xhigh",
ReasoningEffort::Other(s) => s.as_str(),
};
(
Some(wire::ThinkingConfig::Adaptive {
display: Some(wire::ThinkingDisplay::Summarized),
}),
Some(wire::OutputConfig {
effort: Some(effort_str.to_string()),
format: None,
}),
)
}
}
}
#[must_use]
pub fn count_cache_controls(req: &wire::MessagesRequest) -> usize {
let mut count = 0;
if let Some(wire::SystemParam::Blocks(blocks)) = &req.system {
count += blocks.iter().filter(|b| b.cache_control.is_some()).count();
}
for message in &req.messages {
if let wire::MessageContent::Blocks(blocks) = &message.content {
for block in blocks {
match block {
wire::ContentBlock::Text {
cache_control: Some(_),
..
}
| wire::ContentBlock::ToolResult {
cache_control: Some(_),
..
} => count += 1,
_ => {}
}
}
}
}
count
}