use serde_json::{json, Map, Value};
use crate::canonical::{
CanonicalError, CanonicalRequest, Content, ErrorKind, Message, OutputFormat, Role, Tool,
ToolChoice,
};
use crate::protocol::json::finish_body;
use crate::protocol::{ProviderCtx, WireRequest};
mod blocks;
mod cache;
mod count;
pub(super) use count::count as count_body;
pub(super) const REQUEST_PATH: &str = "/v1/messages";
const REASONING_HEADROOM: u32 = 4096;
pub(super) fn encode(
req: &CanonicalRequest,
ctx: &ProviderCtx,
) -> Result<WireRequest, CanonicalError> {
let mut body = Map::new();
body.insert("model".into(), json!(ctx.model));
let mut max_tokens = req.max_tokens.ok_or_else(config_err)?;
if let Some(effort) = req.reasoning {
let budget = effort.budget();
body.insert(
"thinking".into(),
json!({"type": "enabled", "budget_tokens": budget}),
);
max_tokens = max_tokens.max(budget + REASONING_HEADROOM);
}
body.insert("max_tokens".into(), json!(max_tokens));
if let Some(system) = system_value(req)? {
body.insert("system".into(), system);
}
body.insert("messages".into(), messages_value(&req.messages)?);
if req.reasoning.is_none() {
if let Some(t) = req.temperature {
body.insert("temperature".into(), json!(t));
}
if let Some(p) = req.top_p {
body.insert("top_p".into(), json!(p));
}
}
if !req.stop.is_empty() {
body.insert("stop_sequences".into(), json!(req.stop)); }
body.insert("stream".into(), json!(req.stream.unwrap_or(false)));
if !req.tools.is_empty() {
body.insert("tools".into(), tools_value(&req.tools));
}
if let Some(tc) = tool_choice_value(
&req.tool_choice,
req.tools.is_empty(),
req.parallel_tool_calls,
) {
body.insert("tool_choice".into(), tc);
}
match &req.output {
Some(OutputFormat::JsonSchema { schema, .. }) => {
body.insert(
"output_config".into(),
json!({"format": {"type": "json_schema", "schema": schema}}),
);
}
Some(OutputFormat::Json) | None => {}
}
cache::apply(&mut body);
for (k, v) in &req.extra {
body.entry(k.clone()).or_insert_with(|| v.clone()); }
Ok(finish_body(body, format!("{}{REQUEST_PATH}", ctx.base_url)))
}
fn config_err() -> CanonicalError {
CanonicalError {
kind: ErrorKind::Config,
message: "anthropic_messages requires max_tokens".into(),
provider_detail: None,
retry_after_seconds: None,
}
}
pub(super) fn slot_err(slot: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: format!("{slot} accepts only text content"),
provider_detail: None,
retry_after_seconds: None,
}
}
fn system_value(req: &CanonicalRequest) -> Result<Option<Value>, CanonicalError> {
let mut blocks = Vec::new();
if let Some(system) = &req.system {
push_text_blocks(system, &mut blocks)?;
}
for m in &req.messages {
if m.role == Role::System {
push_text_blocks(&m.content, &mut blocks)?;
}
}
Ok((!blocks.is_empty()).then_some(Value::Array(blocks)))
}
fn push_text_blocks(content: &[Content], out: &mut Vec<Value>) -> Result<(), CanonicalError> {
for c in content {
match c {
Content::Text(t) => out.push(json!({"type": "text", "text": t})),
_ => return Err(slot_err("system")),
}
}
Ok(())
}
fn messages_value(msgs: &[Message]) -> Result<Value, CanonicalError> {
let mut out = Vec::new();
for m in msgs {
let role = match m.role {
Role::User | Role::Tool => "user",
Role::Assistant => "assistant",
Role::System => continue, };
let mut blocks = Vec::new();
for c in &m.content {
if let Some(b) = blocks::content_block(c)? {
blocks.push(b);
}
}
out.push(json!({"role": role, "content": Value::Array(blocks)}));
}
Ok(Value::Array(out))
}
fn tools_value(tools: &[Tool]) -> Value {
Value::Array(
tools
.iter()
.map(|t| match t {
Tool::Custom {
name,
description,
input_schema,
strict,
} => {
let mut o = json!({"name": name, "input_schema": input_schema});
if let Some(d) = description {
o["description"] = json!(d);
}
if let Some(s) = strict {
o["strict"] = json!(s); }
o
}
Tool::Provider { kind, name, config } => {
let mut o = json!({"type": kind, "name": name});
for (k, v) in config {
o[k] = v.clone();
}
o
}
})
.collect(),
)
}
fn tool_choice_value(tc: &ToolChoice, no_tools: bool, parallel: Option<bool>) -> Option<Value> {
let mut v = match tc {
ToolChoice::Auto if no_tools => return None,
ToolChoice::Auto => json!({"type": "auto"}),
ToolChoice::Any => json!({"type": "any"}),
ToolChoice::Tool { name } => json!({"type": "tool", "name": name}),
ToolChoice::None => json!({"type": "none"}),
};
if parallel == Some(false) && matches!(tc, ToolChoice::Auto | ToolChoice::Any) {
v["disable_parallel_tool_use"] = json!(true);
}
Some(v)
}