use serde_json::Value;
use crate::canonical::{CanonicalError, ContentKind, Event, FinishReason, Role, Usage};
use crate::protocol::json::{http_error, parse};
use crate::protocol::synth::drain;
use crate::protocol::{DecodeState, Frame};
mod blocks;
mod errors;
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 = parse(&frame.data)?;
if let Some(err) = v["error"].as_str() {
return Ok(vec![Event::Error(errors::stream_error(err))]); }
Ok(line(&v, state))
}
pub(super) fn decode_full(
body: &[u8],
state: &mut DecodeState,
) -> Result<Vec<Event>, CanonicalError> {
Ok(line(&parse(body)?, state))
}
fn line(v: &Value, state: &mut DecodeState) -> Vec<Event> {
let mut out = Vec::new();
if !state.started {
state.started = true;
out.push(Event::message_start(
None, v["model"].as_str().map(str::to_owned),
Role::Assistant,
));
}
let msg = &v["message"];
blocks::thinking(msg, state, &mut out);
blocks::text(msg, state, &mut out);
for call in msg["tool_calls"].as_array().into_iter().flatten() {
blocks::tool_call(call, state, &mut out);
}
if v["done"].as_bool() == Some(true) {
finish(v, state, &mut out); }
out
}
fn finish(v: &Value, state: &mut DecodeState, out: &mut Vec<Event>) {
let reason = finish_reason(v, state);
drain(state, out);
out.push(Event::Usage(usage(v)));
out.push(Event::Finish { reason });
state.terminated = true; }
fn finish_reason(v: &Value, state: &DecodeState) -> FinishReason {
if state
.open
.values()
.any(|b| matches!(b.kind, ContentKind::ToolUse { .. }))
{
return FinishReason::ToolUse;
}
match v["done_reason"].as_str() {
Some("length") => FinishReason::Length,
None | Some("stop") => FinishReason::Stop,
Some(other) => FinishReason::Other(other.to_owned()),
}
}
fn usage(v: &Value) -> Usage {
Usage {
input_tokens: v["prompt_eval_count"].as_u64().map(|x| x as u32),
output_tokens: v["eval_count"].as_u64().map(|x| x as u32),
cache_read_tokens: None,
cache_write_tokens: None,
}
}