refactor: purge Node.js, Rust is the one true backend
- Moved boosapi-rs/* to repo root - Deleted all Node.js source (src/server/, src/web/, scripts/) - Deleted node_modules, package.json, tsconfig, vite.config - Updated .gitignore and AGENTS.md for Rust-only stack - Fixed static file path for frontend dist/web/ - Dockerfile stays the same (already Rust-only) - Kept dist/web/ for Rust static serving
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Anthropic Messages API compatibility switches.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnthropicMessagesCompatibility {
|
||||
/// Value suitable for the `anthropic-version` header when callers need it.
|
||||
pub anthropic_version: Option<String>,
|
||||
/// Messages API requires `max_tokens`; use this when canonical input omits it.
|
||||
pub default_max_tokens: Option<i32>,
|
||||
/// Emit system content as an Anthropic content-block array instead of a string.
|
||||
pub system_as_array: bool,
|
||||
/// Copy canonical metadata to the Anthropic request `metadata` field.
|
||||
pub preserve_metadata: bool,
|
||||
/// Extra provider-specific request fields to merge into the formatted payload.
|
||||
pub extra_request_fields: Option<Value>,
|
||||
}
|
||||
|
||||
impl Default for AnthropicMessagesCompatibility {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
anthropic_version: Some("2023-06-01".to_string()),
|
||||
default_max_tokens: Some(1024),
|
||||
system_as_array: false,
|
||||
preserve_metadata: true,
|
||||
extra_request_fields: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_compatibility() {
|
||||
let compat = AnthropicMessagesCompatibility::default();
|
||||
assert_eq!(compat.anthropic_version.as_deref(), Some("2023-06-01"));
|
||||
assert_eq!(compat.default_max_tokens, Some(1024));
|
||||
assert!(!compat.system_as_array);
|
||||
assert!(compat.preserve_metadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// CanonicalRequest -> Anthropic /v1/messages JSON formatter
|
||||
use crate::transformers::anthropic::messages::AnthropicMessagesCompatibility;
|
||||
use crate::transformers::canonical::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Format a canonical request into Anthropic Messages API JSON.
|
||||
pub fn format_messages_request(request: &CanonicalRequest) -> Value {
|
||||
format_messages_request_with_compat(request, &AnthropicMessagesCompatibility::default())
|
||||
}
|
||||
|
||||
/// Format a canonical request into Anthropic Messages API JSON with compatibility switches.
|
||||
pub fn format_messages_request_with_compat(
|
||||
request: &CanonicalRequest,
|
||||
compatibility: &AnthropicMessagesCompatibility,
|
||||
) -> Value {
|
||||
let mut result = json!({
|
||||
"model": request.model,
|
||||
"messages": format_messages(&request.messages),
|
||||
"stream": request.stream,
|
||||
});
|
||||
|
||||
if let Some(max_tokens) = request.max_tokens.or(compatibility.default_max_tokens) {
|
||||
result["max_tokens"] = json!(max_tokens);
|
||||
}
|
||||
if let Some(temperature) = request.temperature {
|
||||
result["temperature"] = json!(temperature);
|
||||
}
|
||||
if let Some(top_p) = request.top_p {
|
||||
result["top_p"] = json!(top_p);
|
||||
}
|
||||
if let Some(stop) = &request.stop {
|
||||
result["stop_sequences"] = json!(stop);
|
||||
}
|
||||
if let Some(tools) = &request.tools {
|
||||
result["tools"] = json!(format_tools(tools));
|
||||
}
|
||||
if let Some(tool_choice) = &request.tool_choice {
|
||||
if let Some(value) = format_tool_choice(tool_choice) {
|
||||
result["tool_choice"] = value;
|
||||
}
|
||||
}
|
||||
if let Some(reasoning) = &request.reasoning {
|
||||
if let Some(effort) = &reasoning.effort {
|
||||
result["thinking"] = json!({
|
||||
"type": "enabled",
|
||||
"budget_tokens": reasoning_budget_tokens(effort),
|
||||
});
|
||||
}
|
||||
}
|
||||
if compatibility.preserve_metadata {
|
||||
if let Some(metadata) = &request.metadata {
|
||||
result["metadata"] = metadata.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let system_parts = collect_system_parts(&request.messages);
|
||||
if !system_parts.is_empty() {
|
||||
result["system"] = if compatibility.system_as_array {
|
||||
Value::Array(system_parts)
|
||||
} else {
|
||||
Value::String(system_parts_to_text(&system_parts))
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(extra) = &compatibility.extra_request_fields {
|
||||
merge_extra_fields(&mut result, extra);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn format_messages(messages: &[CanonicalMessage]) -> Vec<Value> {
|
||||
messages
|
||||
.iter()
|
||||
.filter(|message| message.role != Role::System)
|
||||
.map(format_message)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_message(message: &CanonicalMessage) -> Value {
|
||||
let role = match message.role {
|
||||
Role::Assistant => "assistant",
|
||||
Role::User | Role::Tool | Role::System => "user",
|
||||
};
|
||||
|
||||
let mut content = match message.role {
|
||||
Role::Tool => format_tool_result_content(message),
|
||||
_ => format_content_parts(&message.content),
|
||||
};
|
||||
|
||||
if let Some(tool_calls) = &message.tool_calls {
|
||||
content.extend(tool_calls.iter().map(format_tool_use));
|
||||
}
|
||||
|
||||
json!({
|
||||
"role": role,
|
||||
"content": content,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_system_parts(messages: &[CanonicalMessage]) -> Vec<Value> {
|
||||
messages
|
||||
.iter()
|
||||
.filter(|message| message.role == Role::System)
|
||||
.flat_map(|message| format_content_parts(&message.content))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn system_parts_to_text(parts: &[Value]) -> String {
|
||||
parts
|
||||
.iter()
|
||||
.filter_map(|part| part["text"].as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn format_content_parts(parts: &[ContentPart]) -> Vec<Value> {
|
||||
parts
|
||||
.iter()
|
||||
.map(|part| match part {
|
||||
ContentPart::Text { text } => json!({
|
||||
"type": "text",
|
||||
"text": text,
|
||||
}),
|
||||
ContentPart::Image { url, .. } => json!({
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": url,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_tool_result_content(message: &CanonicalMessage) -> Vec<Value> {
|
||||
let content = if message.content.len() == 1 {
|
||||
match &message.content[0] {
|
||||
ContentPart::Text { text } => Value::String(text.clone()),
|
||||
_ => Value::Array(format_content_parts(&message.content)),
|
||||
}
|
||||
} else {
|
||||
Value::Array(format_content_parts(&message.content))
|
||||
};
|
||||
|
||||
vec![json!({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": message.tool_call_id.clone().unwrap_or_default(),
|
||||
"content": content,
|
||||
})]
|
||||
}
|
||||
|
||||
fn format_tool_use(tool_call: &ToolCall) -> Value {
|
||||
json!({
|
||||
"type": "tool_use",
|
||||
"id": tool_call.id,
|
||||
"name": tool_call.name,
|
||||
"input": tool_call.arguments,
|
||||
})
|
||||
}
|
||||
|
||||
fn format_tools(tools: &[ToolDefinition]) -> Vec<Value> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
let mut value = json!({
|
||||
"name": tool.name,
|
||||
"input_schema": tool.input_schema,
|
||||
});
|
||||
if let Some(description) = &tool.description {
|
||||
value["description"] = json!(description);
|
||||
}
|
||||
value
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_tool_choice(tool_choice: &ToolChoice) -> Option<Value> {
|
||||
match tool_choice {
|
||||
ToolChoice::None => None,
|
||||
ToolChoice::Auto => Some(json!({ "type": "auto" })),
|
||||
ToolChoice::Required => Some(json!({ "type": "any" })),
|
||||
ToolChoice::Specific(name) => Some(json!({ "type": "tool", "name": name })),
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_budget_tokens(effort: &str) -> i32 {
|
||||
match effort {
|
||||
"low" => 1024,
|
||||
"medium" => 4096,
|
||||
"high" => 8192,
|
||||
_ => 2048,
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_extra_fields(target: &mut Value, extra: &Value) {
|
||||
let (Some(target), Some(extra)) = (target.as_object_mut(), extra.as_object()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (key, value) in extra {
|
||||
target.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_format_simple_request() {
|
||||
let req = CanonicalRequest::chat("claude-3-5-sonnet-20241022", "Hello");
|
||||
let json = format_messages_request(&req);
|
||||
assert_eq!(json["model"], "claude-3-5-sonnet-20241022");
|
||||
assert_eq!(json["max_tokens"], 1024);
|
||||
assert_eq!(json["messages"][0]["role"], "user");
|
||||
assert_eq!(json["messages"][0]["content"][0]["text"], "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_system_and_tools() {
|
||||
let mut req = CanonicalRequest::chat("claude", "Use a tool");
|
||||
req.messages.insert(0, CanonicalMessage::system("Be brief"));
|
||||
req.tools = Some(vec![ToolDefinition {
|
||||
name: "search".to_string(),
|
||||
description: Some("Search docs".to_string()),
|
||||
input_schema: json!({"type": "object"}),
|
||||
}]);
|
||||
req.tool_choice = Some(ToolChoice::Specific("search".to_string()));
|
||||
|
||||
let json = format_messages_request(&req);
|
||||
assert_eq!(json["system"], "Be brief");
|
||||
assert_eq!(json["tools"][0]["name"], "search");
|
||||
assert_eq!(json["tool_choice"]["type"], "tool");
|
||||
assert_eq!(json["tool_choice"]["name"], "search");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_tool_call_and_result() {
|
||||
let assistant = CanonicalMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![],
|
||||
name: None,
|
||||
tool_calls: Some(vec![ToolCall {
|
||||
id: "toolu_1".to_string(),
|
||||
name: "calc".to_string(),
|
||||
arguments: json!({"x": 1}),
|
||||
}]),
|
||||
tool_call_id: None,
|
||||
};
|
||||
let tool = CanonicalMessage::tool("2", "toolu_1");
|
||||
let req = CanonicalRequest {
|
||||
model: "claude".to_string(),
|
||||
messages: vec![assistant, tool],
|
||||
stream: false,
|
||||
max_tokens: Some(10),
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
stop: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = format_messages_request(&req);
|
||||
assert_eq!(json["messages"][0]["content"][0]["type"], "tool_use");
|
||||
assert_eq!(json["messages"][1]["content"][0]["type"], "tool_result");
|
||||
assert_eq!(json["messages"][1]["content"][0]["tool_use_id"], "toolu_1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Transformers - Anthropic Messages
|
||||
pub mod compatibility;
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
pub mod stream;
|
||||
|
||||
pub use compatibility::AnthropicMessagesCompatibility;
|
||||
pub use inbound::{format_messages_request, format_messages_request_with_compat};
|
||||
pub use outbound::parse_messages_response;
|
||||
pub use stream::parse_stream_event;
|
||||
@@ -0,0 +1,143 @@
|
||||
// Anthropic /v1/messages response -> CanonicalResponse parser
|
||||
use crate::transformers::canonical::*;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Parse an Anthropic Messages API response body into canonical format.
|
||||
pub fn parse_messages_response(body: &Value) -> Result<CanonicalResponse, String> {
|
||||
let id = body["id"].as_str().unwrap_or_default().to_string();
|
||||
let model = body["model"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "model is required".to_string())?
|
||||
.to_string();
|
||||
|
||||
let content = body["content"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "content is required".to_string())?;
|
||||
|
||||
let mut text_parts = Vec::new();
|
||||
let mut tool_calls = Vec::new();
|
||||
|
||||
for part in content {
|
||||
match part["type"].as_str() {
|
||||
Some("text") => {
|
||||
if let Some(text) = part["text"].as_str() {
|
||||
text_parts.push(ContentPart::Text {
|
||||
text: text.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Some("tool_use") => {
|
||||
let Some(id) = part["id"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(name) = part["name"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
tool_calls.push(ToolCall {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
arguments: part["input"].clone(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let message = CanonicalMessage {
|
||||
role: Role::Assistant,
|
||||
content: text_parts,
|
||||
name: None,
|
||||
tool_calls: if tool_calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool_calls)
|
||||
},
|
||||
tool_call_id: None,
|
||||
};
|
||||
|
||||
Ok(CanonicalResponse {
|
||||
id,
|
||||
model,
|
||||
choices: vec![CanonicalChoice {
|
||||
index: 0,
|
||||
message,
|
||||
finish_reason: body["stop_reason"].as_str().map(normalize_stop_reason),
|
||||
}],
|
||||
usage: parse_usage(body.get("usage")),
|
||||
metadata: Some(body.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_usage(value: Option<&Value>) -> Option<Usage> {
|
||||
let usage = value?.as_object()?;
|
||||
let prompt_tokens = usage.get("input_tokens").and_then(as_i32);
|
||||
let completion_tokens = usage.get("output_tokens").and_then(as_i32);
|
||||
Some(Usage {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
total_tokens: prompt_tokens.zip(completion_tokens).map(|(a, b)| a + b),
|
||||
})
|
||||
}
|
||||
|
||||
fn as_i32(value: &Value) -> Option<i32> {
|
||||
value.as_i64().map(|n| n as i32)
|
||||
}
|
||||
|
||||
fn normalize_stop_reason(reason: &str) -> String {
|
||||
match reason {
|
||||
"end_turn" | "stop_sequence" => "stop".to_string(),
|
||||
"max_tokens" => "length".to_string(),
|
||||
"tool_use" => "tool_calls".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_response() {
|
||||
let body = json!({
|
||||
"id": "msg_123",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude",
|
||||
"content": [{"type": "text", "text": "Hello"}],
|
||||
"stop_reason": "end_turn",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 3}
|
||||
});
|
||||
|
||||
let response = parse_messages_response(&body).unwrap();
|
||||
assert_eq!(response.id, "msg_123");
|
||||
assert_eq!(response.model, "claude");
|
||||
assert_eq!(response.choices[0].finish_reason.as_deref(), Some("stop"));
|
||||
assert_eq!(response.usage.unwrap().total_tokens, Some(13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tool_use_response() {
|
||||
let body = json!({
|
||||
"id": "msg_123",
|
||||
"model": "claude",
|
||||
"content": [{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_1",
|
||||
"name": "search",
|
||||
"input": {"q": "rust"}
|
||||
}],
|
||||
"stop_reason": "tool_use"
|
||||
});
|
||||
|
||||
let response = parse_messages_response(&body).unwrap();
|
||||
let tool_calls = response.choices[0].message.tool_calls.as_ref().unwrap();
|
||||
assert_eq!(tool_calls[0].id, "toolu_1");
|
||||
assert_eq!(tool_calls[0].name, "search");
|
||||
assert_eq!(tool_calls[0].arguments["q"], "rust");
|
||||
assert_eq!(
|
||||
response.choices[0].finish_reason.as_deref(),
|
||||
Some("tool_calls")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Anthropic Messages SSE stream parser
|
||||
use crate::transformers::canonical::*;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Parse a single Anthropic Messages SSE event into a canonical stream event.
|
||||
///
|
||||
/// Accepts either a full SSE frame (`event: ...\ndata: ...`) or a raw JSON data payload.
|
||||
pub fn parse_stream_event(frame: &str) -> Option<CanonicalStreamEvent> {
|
||||
let (event_name, data) = parse_sse_frame(frame)?;
|
||||
let parsed: Value = serde_json::from_str(&data).ok()?;
|
||||
let event_type = event_name
|
||||
.as_deref()
|
||||
.or_else(|| parsed["type"].as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
match event_type {
|
||||
"message_start" => parse_message_start(&parsed),
|
||||
"content_block_delta" => parse_content_block_delta(&parsed),
|
||||
"content_block_stop" => Some(empty_event()),
|
||||
"message_delta" => parse_message_delta(&parsed),
|
||||
"message_stop" => Some(empty_event()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sse_frame(frame: &str) -> Option<(Option<String>, String)> {
|
||||
let trimmed = frame.trim();
|
||||
if trimmed.is_empty() || trimmed == "data: [DONE]" || trimmed == "[DONE]" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if trimmed.starts_with('{') {
|
||||
return Some((None, trimmed.to_string()));
|
||||
}
|
||||
|
||||
let mut event_name = None;
|
||||
let mut data_lines = Vec::new();
|
||||
|
||||
for line in trimmed.lines() {
|
||||
if let Some(value) = line.strip_prefix("event:") {
|
||||
event_name = Some(value.trim().to_string());
|
||||
} else if let Some(value) = line.strip_prefix("data:") {
|
||||
data_lines.push(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if data_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((event_name, data_lines.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message_start(parsed: &Value) -> Option<CanonicalStreamEvent> {
|
||||
let message = parsed.get("message")?;
|
||||
Some(CanonicalStreamEvent {
|
||||
id: message["id"].as_str().map(|s| s.to_string()),
|
||||
choices: vec![CanonicalStreamChoice {
|
||||
index: 0,
|
||||
delta: StreamDelta {
|
||||
role: Some(Role::Assistant),
|
||||
content: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
finish_reason: None,
|
||||
}],
|
||||
usage: parse_usage(message.get("usage")),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_content_block_delta(parsed: &Value) -> Option<CanonicalStreamEvent> {
|
||||
let index = parsed["index"].as_i64().unwrap_or(0) as i32;
|
||||
let delta = parsed.get("delta")?;
|
||||
|
||||
let stream_delta = match delta["type"].as_str()? {
|
||||
"text_delta" => StreamDelta {
|
||||
role: None,
|
||||
content: delta["text"].as_str().map(|s| s.to_string()),
|
||||
tool_calls: None,
|
||||
},
|
||||
"input_json_delta" => StreamDelta {
|
||||
role: None,
|
||||
content: None,
|
||||
tool_calls: Some(vec![StreamToolCall {
|
||||
index,
|
||||
id: None,
|
||||
name: None,
|
||||
arguments: delta["partial_json"].as_str().map(|s| s.to_string()),
|
||||
}]),
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(CanonicalStreamEvent {
|
||||
id: None,
|
||||
choices: vec![CanonicalStreamChoice {
|
||||
index,
|
||||
delta: stream_delta,
|
||||
finish_reason: None,
|
||||
}],
|
||||
usage: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_message_delta(parsed: &Value) -> Option<CanonicalStreamEvent> {
|
||||
let delta = parsed.get("delta")?;
|
||||
let finish_reason = delta["stop_reason"].as_str().map(normalize_stop_reason);
|
||||
|
||||
Some(CanonicalStreamEvent {
|
||||
id: None,
|
||||
choices: vec![CanonicalStreamChoice {
|
||||
index: 0,
|
||||
delta: StreamDelta {
|
||||
role: None,
|
||||
content: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
finish_reason,
|
||||
}],
|
||||
usage: parse_usage(parsed.get("usage")),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_usage(value: Option<&Value>) -> Option<Usage> {
|
||||
let usage = value?.as_object()?;
|
||||
let prompt_tokens = usage.get("input_tokens").and_then(as_i32);
|
||||
let completion_tokens = usage.get("output_tokens").and_then(as_i32);
|
||||
Some(Usage {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
total_tokens: prompt_tokens.zip(completion_tokens).map(|(a, b)| a + b),
|
||||
})
|
||||
}
|
||||
|
||||
fn as_i32(value: &Value) -> Option<i32> {
|
||||
value.as_i64().map(|n| n as i32)
|
||||
}
|
||||
|
||||
fn normalize_stop_reason(reason: &str) -> String {
|
||||
match reason {
|
||||
"end_turn" | "stop_sequence" => "stop".to_string(),
|
||||
"max_tokens" => "length".to_string(),
|
||||
"tool_use" => "tool_calls".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_event() -> CanonicalStreamEvent {
|
||||
CanonicalStreamEvent {
|
||||
id: None,
|
||||
choices: vec![],
|
||||
usage: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_start() {
|
||||
let frame = r#"event: message_start
|
||||
data: {"type":"message_start","message":{"id":"msg_1","model":"claude","role":"assistant","content":[],"usage":{"input_tokens":4,"output_tokens":1}}}"#;
|
||||
let event = parse_stream_event(frame).unwrap();
|
||||
assert_eq!(event.id.as_deref(), Some("msg_1"));
|
||||
assert_eq!(event.choices[0].delta.role, Some(Role::Assistant));
|
||||
assert_eq!(event.usage.unwrap().total_tokens, Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_text_delta() {
|
||||
let frame = r#"event: content_block_delta
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#;
|
||||
let event = parse_stream_event(frame).unwrap();
|
||||
assert_eq!(event.choices[0].index, 0);
|
||||
assert_eq!(event.choices[0].delta.content.as_deref(), Some("Hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tool_input_delta() {
|
||||
let frame = r#"data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"q\""}}"#;
|
||||
let event = parse_stream_event(frame).unwrap();
|
||||
let tool_calls = event.choices[0].delta.tool_calls.as_ref().unwrap();
|
||||
assert_eq!(tool_calls[0].index, 1);
|
||||
assert_eq!(tool_calls[0].arguments.as_deref(), Some("{\"q\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_delta() {
|
||||
let frame = r#"event: message_delta
|
||||
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens"},"usage":{"output_tokens":9}}"#;
|
||||
let event = parse_stream_event(frame).unwrap();
|
||||
assert_eq!(event.choices[0].finish_reason.as_deref(), Some("length"));
|
||||
assert_eq!(event.usage.unwrap().completion_tokens, Some(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_message_stop() {
|
||||
let frame = r#"event: message_stop
|
||||
data: {"type":"message_stop"}"#;
|
||||
let event = parse_stream_event(frame).unwrap();
|
||||
assert!(event.choices.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Transformers - Anthropic
|
||||
pub mod messages;
|
||||
|
||||
pub use messages::{
|
||||
format_messages_request, format_messages_request_with_compat, parse_messages_response,
|
||||
parse_stream_event, AnthropicMessagesCompatibility,
|
||||
};
|
||||
Reference in New Issue
Block a user