use serde_json::{json, Value};
use crate::canonical::{CanonicalError, Event, FinishReason, Role, Usage};
use crate::protocol::json::{http_error, parse, text_of, to_json_string};
use crate::protocol::{DecodeState, Frame};
mod blocks;
mod errors;
pub(super) fn decode_full(
body: &[u8],
state: &mut DecodeState,
) -> Result<Vec<Event>, CanonicalError> {
let v = parse(body)?;
let mut out = message_start(&json!({ "message": v }));
for (index, block) in v["content"].as_array().into_iter().flatten().enumerate() {
explode_block(index as u32, block, state, &mut out);
}
out.extend(message_delta(&json!({ "delta": {
"stop_reason": v["stop_reason"],
"stop_sequence": v["stop_sequence"],
"stop_details": v["stop_details"],
}})));
Ok(out)
}
fn explode_block(index: u32, block: &Value, state: &mut DecodeState, out: &mut Vec<Event>) {
let start = json!({ "index": index, "content_block": block });
out.extend(blocks::content_block_start(&start, state));
let mut deltas = Vec::new();
match block["type"].as_str().unwrap_or_default() {
"text" => deltas.push(json!({ "type": "text_delta", "text": block["text"] })),
"tool_use" | "server_tool_use" => deltas.push(
json!({ "type": "input_json_delta", "partial_json": to_json_string(&block["input"]) }),
),
"thinking" => {
deltas.push(json!({ "type": "thinking_delta", "thinking": block["thinking"] }));
if let Some(sig) = block["signature"].as_str().filter(|s| !s.is_empty()) {
deltas.push(json!({ "type": "signature_delta", "signature": sig }));
}
}
_ => {}
}
for delta in deltas {
out.extend(blocks::content_block_delta(
&json!({ "index": index, "delta": delta }),
state,
));
}
out.extend(blocks::content_block_stop(
&json!({ "index": index }),
state,
));
}
pub(super) fn decode(frame: Frame, state: &mut DecodeState) -> Result<Vec<Event>, CanonicalError> {
if let Some(status) = frame.status {
return Ok(vec![Event::Error(http_error(&frame.data, status))]);
}
let v: Value = parse(&frame.data)?;
Ok(match v["type"].as_str().unwrap_or_default() {
"message_start" => message_start(&v),
"content_block_start" => blocks::content_block_start(&v, state),
"content_block_delta" => blocks::content_block_delta(&v, state),
"content_block_stop" => blocks::content_block_stop(&v, state),
"message_delta" => message_delta(&v),
"message_stop" => {
state.terminated = true; vec![]
}
"error" => vec![Event::Error(errors::error_value(&v))], _ => vec![],
})
}
fn message_start(v: &Value) -> Vec<Event> {
let m = &v["message"];
let mut out = vec![Event::message_start(
m["id"].as_str().map(str::to_owned),
m["model"].as_str().map(str::to_owned),
Role::Assistant, )];
if let Some(u) = m.get("usage").filter(|u| u.is_object()) {
out.push(Event::Usage(usage(u)));
}
out
}
fn message_delta(v: &Value) -> Vec<Event> {
let mut out = Vec::new();
if let Some(u) = v.get("usage").filter(|u| u.is_object()) {
out.push(Event::Usage(usage(u)));
}
let d = &v["delta"];
if let Some(reason) = d["stop_reason"].as_str() {
out.push(Event::Finish {
reason: finish_reason(reason, d),
});
}
out
}
fn finish_reason(reason: &str, d: &Value) -> FinishReason {
match reason {
"end_turn" => FinishReason::Stop,
"max_tokens" => FinishReason::Length,
"stop_sequence" => FinishReason::StopSequence,
"tool_use" => FinishReason::ToolUse,
"pause_turn" => FinishReason::Pause,
"refusal" => {
let sd = &d["stop_details"];
FinishReason::Refusal {
category: text_of(sd, "category"),
explanation: sd["explanation"].as_str().map(str::to_owned),
}
}
other => FinishReason::Other(other.to_owned()),
}
}
fn usage(u: &Value) -> Usage {
let field = |k: &str| u[k].as_u64().map(|x| x as u32);
Usage {
input_tokens: field("input_tokens"),
output_tokens: field("output_tokens"),
cache_write_tokens: field("cache_creation_input_tokens"),
cache_read_tokens: field("cache_read_input_tokens"),
}
}