use axum::response::sse::Event;
use super::helpers::*;
#[derive(Debug, Clone, PartialEq)]
pub(super) struct SseEvent {
pub(super) event: String,
pub(super) data: serde_json::Value,
}
impl SseEvent {
pub(super) fn to_axum_event(&self) -> Event {
Event::default()
.event(&self.event)
.data(serde_json::to_string(&self.data).unwrap_or_default())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum OpenBlock {
None,
Text,
Thinking,
ToolUse(usize),
}
pub struct AnthropicTranslator {
model: String,
msg_started: bool,
msg_id: String,
block_idx: u32,
open_block: OpenBlock,
tool_started: std::collections::HashMap<usize, ()>,
completion_tokens: usize,
prompt_tokens: usize,
cached_prompt_tokens: usize,
finished: bool,
}
fn error_detail(payload: &str) -> String {
serde_json::from_str::<serde_json::Value>(payload)
.ok()
.and_then(|v| v["error"]["message"].as_str().map(str::to_string))
.unwrap_or_else(|| payload.to_string())
}
impl AnthropicTranslator {
pub(super) fn new(model: String) -> Self {
Self {
model,
msg_started: false,
msg_id: format!("msg_{}", crate::ids::uuid_v4()),
block_idx: 0,
open_block: OpenBlock::None,
tool_started: std::collections::HashMap::new(),
completion_tokens: 0,
prompt_tokens: 0,
cached_prompt_tokens: 0,
finished: false,
}
}
fn final_usage(&self) -> serde_json::Value {
serde_json::json!({
"input_tokens": self.prompt_tokens,
"cache_read_input_tokens": self.cached_prompt_tokens,
"output_tokens": self.completion_tokens,
})
}
fn make_event(ev_type: &str, data: serde_json::Value) -> SseEvent {
SseEvent {
event: ev_type.to_string(),
data,
}
}
pub(super) fn close_open_block(&mut self) -> Option<SseEvent> {
match self.open_block {
OpenBlock::None => None,
OpenBlock::Text | OpenBlock::Thinking => {
let ev = Self::make_event(
"content_block_stop",
serde_json::json!({
"type": "content_block_stop",
"index": self.block_idx,
}),
);
self.open_block = OpenBlock::None;
self.block_idx += 1;
Some(ev)
}
OpenBlock::ToolUse(oa_idx) => {
self.tool_started.remove(&oa_idx);
let ev = Self::make_event(
"content_block_stop",
serde_json::json!({
"type": "content_block_stop",
"index": self.block_idx,
}),
);
self.open_block = OpenBlock::None;
self.block_idx += 1;
Some(ev)
}
}
}
pub(super) fn ensure_message_start(&mut self, out: &mut Vec<SseEvent>) {
if self.msg_started {
return;
}
let id = self.msg_id.clone();
out.push(Self::make_event(
"message_start",
serde_json::json!({
"type": "message_start",
"message": {
"id": id,
"type": "message",
"role": "assistant",
"model": self.model,
"content": [],
"stop_reason": serde_json::Value::Null,
"stop_sequence": serde_json::Value::Null,
"usage": {
"input_tokens": self.prompt_tokens,
"output_tokens": 0,
},
},
}),
));
self.msg_started = true;
}
pub(super) fn on_delta(&mut self, d: &crate::ir::StreamDelta, out: &mut Vec<SseEvent>) {
use crate::ir::StreamDelta;
match d {
StreamDelta::Reasoning { text, .. } if !text.is_empty() => {
self.ensure_message_start(out);
if !matches!(self.open_block, OpenBlock::Thinking) {
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"content_block_start",
serde_json::json!({
"type": "content_block_start",
"index": self.block_idx,
"content_block": {"type": "thinking", "thinking": ""},
}),
));
self.open_block = OpenBlock::Thinking;
}
out.push(Self::make_event(
"content_block_delta",
serde_json::json!({
"type": "content_block_delta",
"index": self.block_idx,
"delta": {"type": "thinking_delta", "thinking": text},
}),
));
}
StreamDelta::Content { text, .. } if !text.is_empty() => {
self.ensure_message_start(out);
if !matches!(self.open_block, OpenBlock::Text) {
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"content_block_start",
serde_json::json!({
"type": "content_block_start",
"index": self.block_idx,
"content_block": {"type": "text", "text": ""},
}),
));
self.open_block = OpenBlock::Text;
}
out.push(Self::make_event(
"content_block_delta",
serde_json::json!({
"type": "content_block_delta",
"index": self.block_idx,
"delta": {"type": "text_delta", "text": text},
}),
));
}
StreamDelta::ToolCallStart { index, id, name } => {
self.ensure_message_start(out);
let need_start = !self.tool_started.contains_key(index);
if need_start
&& !matches!(self.open_block, OpenBlock::ToolUse(idx) if idx == *index)
{
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"content_block_start",
serde_json::json!({
"type": "content_block_start",
"index": self.block_idx,
"content_block": {
"type": "tool_use",
"id": id,
"name": name,
"input": {},
},
}),
));
self.open_block = OpenBlock::ToolUse(*index);
self.tool_started.insert(*index, ());
}
}
StreamDelta::ToolCallArgs {
index, fragment, ..
} => {
if fragment.is_empty() {
return;
}
self.ensure_message_start(out);
if !matches!(self.open_block, OpenBlock::ToolUse(idx) if idx == *index) {
tracing::warn!(
target: "anthropic_translator",
oa_idx = index,
current_block = ?self.open_block,
arg_fragment_len = fragment.len(),
"dropping tool-call argument fragment for non-open tool block"
);
return;
}
out.push(Self::make_event(
"content_block_delta",
serde_json::json!({
"type": "content_block_delta",
"index": self.block_idx,
"delta": {
"type": "input_json_delta",
"partial_json": fragment,
},
}),
));
}
StreamDelta::Finish { reason, usage, .. } => {
if self.finished {
return;
}
self.prompt_tokens = usage.prompt_tokens;
self.completion_tokens = usage.completion_tokens;
self.cached_prompt_tokens = usage.cached_prompt_tokens;
self.ensure_message_start(out);
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"message_delta",
serde_json::json!({
"type": "message_delta",
"delta": {
"stop_reason": convert_stop_reason(reason.as_wire()),
"stop_sequence": serde_json::Value::Null,
},
"usage": self.final_usage(),
}),
));
out.push(Self::make_event(
"message_stop",
serde_json::json!({"type": "message_stop"}),
));
self.finished = true;
}
StreamDelta::Refusal { .. } => {}
StreamDelta::Error { message } => {
self.ensure_message_start(out);
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"error",
serde_json::json!({
"type": "error",
"error": {"type": "api_error", "message": error_detail(message)},
}),
));
self.finished = true;
}
StreamDelta::Content { .. } | StreamDelta::Reasoning { .. } => {}
}
}
pub(super) fn finalize(&mut self, out: &mut Vec<SseEvent>) {
if self.finished {
return;
}
self.ensure_message_start(out);
if let Some(stop) = self.close_open_block() {
out.push(stop);
}
out.push(Self::make_event(
"message_delta",
serde_json::json!({
"type": "message_delta",
"delta": {
"stop_reason": "max_tokens",
"stop_sequence": serde_json::Value::Null,
},
"usage": self.final_usage(),
}),
));
out.push(Self::make_event(
"message_stop",
serde_json::json!({"type": "message_stop"}),
));
self.finished = true;
}
}