///|
/// Adapter for the Anthropic Messages API.
///
/// Anthropic's `/v1/messages` endpoint differs from the OpenAI chat API:
///
/// - The `system` prompt is a top-level field, not a message with role
/// `system`.
/// - `max_tokens` is required.
/// - The response carries a top-level `content` array of blocks, and usage is
/// reported as `input_tokens` / `output_tokens`.
///
/// This module converts a `ChatRequest` (OpenAI-shaped) into an Anthropic
/// request body, and converts an Anthropic response back into the common
/// `ChatResponse`, so the rest of the SDK can stay provider-agnostic.
///|
/// Build an Anthropic Messages request body from a common `ChatRequest`.
///
/// System messages are hoisted into the top-level `system` field; all other
/// messages are mapped to Anthropic's `{role, content}` shape. Anthropic
/// requires `max_tokens`, so a default of 1024 is used when unset.
pub fn anthropic_request_body(request : ChatRequest) -> Json {
let system = StringBuilder::new()
let messages = []
for msg in request.messages {
match msg.role {
System =>
match msg.content {
Str(s) => {
if system.to_string().length() > 0 {
system.write_string("\n\n")
}
system.write_string(s)
}
Parts(_) => ()
}
_ =>
messages.push(
Json::object({
"role": Json::string(anthropic_role(msg.role)),
"content": anthropic_content(msg.content),
}),
)
}
}
let obj : Map[String, Json] = {
"model": Json::string(request.model),
"messages": Json::array(messages),
"max_tokens": Json::number(request.max_tokens.unwrap_or(1024).to_double()),
}
let sys = system.to_string()
if sys.length() > 0 {
obj["system"] = Json::string(sys)
}
if request.temperature is Some(t) {
obj["temperature"] = Json::number(t)
}
if request.top_p is Some(p) {
obj["top_p"] = Json::number(p)
}
if request.stop is Some(s) {
obj["stop_sequences"] = s.to_json()
}
Json::object(obj)
}
///|
/// Map a common `Role` to the Anthropic role string. Anthropic only supports
/// `user` and `assistant` turns; `tool` results are represented as `user`.
fn anthropic_role(role : Role) -> String {
match role {
Assistant => "assistant"
_ => "user"
}
}
///|
/// Convert message content into an Anthropic content value.
fn anthropic_content(content : Content) -> Json {
match content {
Str(s) => Json::string(s)
Parts(parts) => {
let arr = []
for p in parts {
match p {
Text(t) =>
arr.push(
Json::object({
"type": Json::string("text"),
"text": Json::string(t),
}),
)
ImageUrl(u) =>
arr.push(
Json::object({
"type": Json::string("image"),
"source": Json::object({
"type": Json::string("url"),
"url": Json::string(u),
}),
}),
)
}
}
Json::array(arr)
}
}
}
///|
/// Parse an Anthropic Messages response into the common `ChatResponse`.
///
/// The `content` blocks of type `text` are concatenated into a single
/// assistant message; usage is mapped from `input_tokens`/`output_tokens`.
pub fn parse_anthropic_response(json : Json) -> ChatResponse raise LLMError {
guard json is Object(obj) else {
raise Decode("anthropic response: expected object")
}
let id = match obj.get("id") {
Some(String(s)) => s
_ => ""
}
let model = match obj.get("model") {
Some(String(s)) => s
_ => ""
}
let text = StringBuilder::new()
match obj.get("content") {
Some(Array(blocks)) =>
for block in blocks {
match block {
Object(b) =>
match b.get("type") {
Some(String("text")) =>
match b.get("text") {
Some(String(t)) => text.write_string(t)
_ => ()
}
_ => ()
}
_ => ()
}
}
_ => ()
}
let finish_reason = match obj.get("stop_reason") {
Some(String(s)) => Some(anthropic_stop_reason(s))
_ => None
}
let usage = match obj.get("usage") {
Some(Object(u)) => {
let input = match u.get("input_tokens") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let output = match u.get("output_tokens") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
Some(Usage::{
prompt_tokens: input,
completion_tokens: output,
total_tokens: input + output,
})
}
_ => None
}
let message = Message::assistant(text.to_string())
{
id,
object: "chat.completion",
created: 0L,
model,
choices: [{ index: 0, message, finish_reason }],
usage,
system_fingerprint: None,
}
}
///|
/// Map an Anthropic `stop_reason` to the OpenAI-style `finish_reason`.
fn anthropic_stop_reason(reason : String) -> String {
match reason {
"end_turn" => "stop"
"max_tokens" => "length"
"stop_sequence" => "stop"
"tool_use" => "tool_calls"
other => other
}
}
///|
/// Perform a chat completion against an Anthropic Messages endpoint.
///
/// This assumes the client's `base_url` targets an Anthropic-compatible host
/// (default `https://api.anthropic.com/v1`). Note that Anthropic requires the
/// `x-api-key` and `anthropic-version` headers; configure these via a client
/// whose auth is set accordingly, or use this against a proxy that injects
/// them. The request/response translation is handled here.
pub async fn Client::chat_anthropic(
self : Client,
request : ChatRequest,
) -> ChatResponse raise LLMError {
let json = self.post_json("/messages", anthropic_request_body(request))
parse_anthropic_response(json)
}