use serde_json::Value;
use crate::canonical::{ContentKind, Delta, Event, FinishReason};
use crate::protocol::json::{nonempty, text_of};
use crate::protocol::synth::{drain, next_index, open_text};
use crate::protocol::{DecodeState, OpenBlock};
pub(super) fn text(delta: &Value, state: &mut DecodeState, out: &mut Vec<Event>) {
let Some(t) = nonempty(&delta["content"]) else {
return;
};
let index = open_text(state, out);
out.push(Event::ContentDelta {
index,
delta: Delta::TextDelta(t.to_owned()),
});
}
pub(super) fn tool_call(call: &Value, state: &mut DecodeState, out: &mut Vec<Event>) {
let t = call["index"].as_u64().unwrap_or(0) as u32;
let index = match state.tool_index.get(&t) {
Some(&c) => c,
None => {
let c = next_index(state);
let kind = ContentKind::ToolUse {
id: text_of(call, "id"),
name: text_of(&call["function"], "name"),
};
state.tool_index.insert(t, c);
state.open.insert(c, OpenBlock { kind: kind.clone() });
out.push(Event::ContentStart { index: c, kind });
c
}
};
if let Some(arg) = nonempty(&call["function"]["arguments"]) {
out.push(Event::ContentDelta {
index,
delta: Delta::JsonDelta(arg.to_owned()),
});
}
}
pub(super) fn finish(reason: &str, state: &mut DecodeState, out: &mut Vec<Event>) {
drain(state, out);
out.push(Event::Finish {
reason: finish_reason(reason, &state.refusal),
});
}
fn finish_reason(reason: &str, refusal: &str) -> FinishReason {
if !refusal.is_empty() {
return FinishReason::Refusal {
category: "refusal".into(),
explanation: Some(refusal.to_owned()),
};
}
match reason {
"stop" => FinishReason::Stop,
"length" => FinishReason::Length,
"tool_calls" | "function_call" => FinishReason::ToolUse,
"content_filter" => FinishReason::Refusal {
category: "content_filter".into(),
explanation: None,
},
other => FinishReason::Other(other.to_owned()),
}
}