use serde_json::{json, Value};
use crate::canonical::{CanonicalError, Event, Role, Usage};
use crate::protocol::json::{http_error, nonempty, parse};
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 choice = &v["choices"][0];
chunk(
&json!({ "id": v["id"], "model": v["model"], "usage": v["usage"], "choices": [{
"delta": as_delta(&choice["message"]),
"finish_reason": choice["finish_reason"],
}]}),
state,
)
}
fn as_delta(message: &Value) -> Value {
let calls: Vec<Value> = message["tool_calls"]
.as_array()
.into_iter()
.flatten()
.enumerate()
.map(|(i, call)| {
let mut call = call.clone();
call["index"] = json!(i);
call
})
.collect();
json!({
"content": message["content"],
"refusal": message["refusal"],
"tool_calls": Value::Array(calls),
})
}
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))]); }
if frame.data == b"[DONE]" {
state.terminated = true; return Ok(vec![]);
}
let v = parse(&frame.data)?;
if v["error"].is_object() {
return Ok(vec![Event::Error(errors::stream_error(&v["error"]))]); }
chunk(&v, state)
}
fn chunk(v: &Value, state: &mut DecodeState) -> Result<Vec<Event>, CanonicalError> {
let mut out = Vec::new();
if !state.started {
state.started = true;
out.push(Event::message_start(
v["id"].as_str().map(str::to_owned),
v["model"].as_str().map(str::to_owned),
Role::Assistant,
));
}
let choice = &v["choices"][0];
let delta = &choice["delta"];
blocks::text(delta, state, &mut out);
if let Some(r) = nonempty(&delta["refusal"]) {
state.refusal.push_str(r); }
if let Some(calls) = delta["tool_calls"].as_array() {
for call in calls {
blocks::tool_call(call, state, &mut out);
}
}
if let Some(reason) = choice["finish_reason"].as_str() {
blocks::finish(reason, state, &mut out); state.terminated = true; }
if let Some(u) = v.get("usage").filter(|u| u.is_object()) {
out.push(Event::Usage(usage(u))); }
Ok(out)
}
fn usage(u: &Value) -> Usage {
Usage {
input_tokens: u["prompt_tokens"].as_u64().map(|x| x as u32),
output_tokens: u["completion_tokens"].as_u64().map(|x| x as u32),
cache_read_tokens: u["prompt_tokens_details"]["cached_tokens"]
.as_u64()
.map(|x| x as u32),
cache_write_tokens: None,
}
}