use llmkit_core::{
ChatRequest, ChatResponse, FinishReason, LlmError, LlmResult, Message, MessageContent, Role,
ToolCall, ToolChoice,
};
use serde_json::json;
use crate::types::*;
const DEFAULT_MAX_TOKENS: u32 = 1024;
pub(crate) fn build_request(
req: &ChatRequest,
model: String,
stream: bool,
) -> MessagesRequest {
let messages = req.messages.iter().map(map_message).collect();
let tools = req.tools.as_ref().map(|ts| {
ts.iter()
.map(|t| WireTool {
name: t.name.clone(),
description: t.description.clone(),
input_schema: t.input_schema.clone(),
})
.collect()
});
let tool_choice = req.tool_choice.as_ref().map(map_tool_choice);
MessagesRequest {
model,
messages,
max_tokens: req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
system: req.system.clone(),
temperature: req.temperature,
stop_sequences: req.stop.clone(),
tools,
tool_choice,
stream,
}
}
fn map_message(m: &Message) -> WireMessage {
match &m.content {
MessageContent::ToolResult { tool_use_id, content } => WireMessage {
role: "user".into(),
content: vec![WireBlock::ToolResult {
tool_use_id: tool_use_id.clone(),
content: content.clone(),
}],
},
MessageContent::ToolUse { id, name, input } => WireMessage {
role: "assistant".into(),
content: vec![WireBlock::ToolUse {
id: id.clone(),
name: name.clone(),
input: input.clone(),
}],
},
other => WireMessage {
role: role_str(m.role).into(),
content: vec![WireBlock::Text { text: other.as_text().unwrap_or_default() }],
},
}
}
fn role_str(role: Role) -> &'static str {
match role {
Role::Assistant => "assistant",
_ => "user",
}
}
fn map_tool_choice(choice: &ToolChoice) -> serde_json::Value {
match choice {
ToolChoice::Auto => json!({ "type": "auto" }),
ToolChoice::Any => json!({ "type": "any" }),
ToolChoice::None => json!({ "type": "none" }),
ToolChoice::Tool(name) => json!({ "type": "tool", "name": name }),
}
}
pub(crate) fn map_stop_reason(reason: Option<&str>) -> FinishReason {
match reason {
Some("end_turn") | Some("stop_sequence") => FinishReason::Stop,
Some("max_tokens") => FinishReason::MaxTokens,
Some("tool_use") => FinishReason::ToolUse,
Some("refusal") => FinishReason::ContentFilter,
Some(other) => FinishReason::Other(other.to_string()),
None => FinishReason::Stop,
}
}
pub(crate) fn map_response(resp: MessagesResponse, latency_ms: u64) -> LlmResult<ChatResponse> {
let mut text = String::new();
let mut tool_calls = Vec::new();
for block in resp.content {
match block {
RespBlock::Text { text: t } => text.push_str(&t),
RespBlock::ToolUse { id, name, input } => {
tool_calls.push(ToolCall::new(id, name, input));
}
RespBlock::Unknown => {}
}
}
let usage = resp
.usage
.map(|u| llmkit_core::TokenUsage::new(u.input_tokens, u.output_tokens))
.unwrap_or_default();
if text.is_empty() && tool_calls.is_empty() && usage.total() == 0 {
return Err(LlmError::Provider { status: 200, message: "empty response".into() });
}
Ok(ChatResponse {
id: resp.id,
provider: "anthropic".into(),
model: resp.model,
message: Message::assistant(text),
finish_reason: map_stop_reason(resp.stop_reason.as_deref()),
tool_calls,
usage,
cost: None,
latency_ms,
})
}