use crate::core::ir::{TypeDef, TypeRef};
use crate::e2e::fixture::{Assertion, Fixture};
#[derive(Default)]
pub(super) struct ContentProbe {
item: String,
choice: String,
delta: String,
text: bool,
finish: bool,
tools: bool,
wire: WireKeys,
}
#[derive(Default, serde::Serialize)]
struct WireKeys {
finish: String,
delta: String,
tools: String,
function: String,
name: String,
}
impl WireKeys {
fn resolve(choice: &TypeDef, delta: Option<&TypeDef>, types: &[TypeDef]) -> Self {
let tool = delta
.and_then(|owner| field_type(owner, "tool_calls"))
.and_then(list_element);
let tool = tool.and_then(|name| types.iter().find(|ty| ty.name == name && !ty.binding_excluded));
let function = tool.and_then(|owner| field_type(owner, "function")).and_then(named);
let function = function.and_then(|name| types.iter().find(|ty| ty.name == name && !ty.binding_excluded));
Self {
finish: wire_key(Some(choice), "finish_reason"),
delta: wire_key(Some(choice), "delta"),
tools: wire_key(delta, "tool_calls"),
function: wire_key(tool, "function"),
name: wire_key(function, "name"),
}
}
}
fn wire_key(owner: Option<&TypeDef>, name: &str) -> String {
let field =
owner.and_then(|owner| crate::codegen::shared::binding_fields(&owner.fields).find(|field| field.name == name));
let wire = crate::codegen::naming::wire_field_name(
name,
field.and_then(|field| field.serde_rename.as_deref()),
owner.and_then(|owner| owner.serde_rename_all.as_deref()),
);
let json = serde_json::to_string(&wire).expect("wire name serializes as a JSON string");
escape_c(&json[1..json.len() - 1])
}
fn asserts(fixture: &Fixture, matches: impl Fn(&str) -> bool) -> bool {
fixture
.assertions
.iter()
.any(|assertion| assertion.field.as_deref().is_some_and(&matches))
}
impl ContentProbe {
pub(super) fn has_content(&self) -> bool {
!self.item.is_empty()
}
pub(super) fn resolve(fixture: &Fixture, types: &[TypeDef], item: &str) -> Self {
let wants_text = asserts(fixture, |field| field == "stream_content");
let wants_finish = asserts(fixture, |field| field == "finish_reason" || field == "stream_complete");
let wants_tools = asserts(fixture, |field| {
field == "tool_calls" || field.starts_with("tool_calls[") || field.starts_with("tool_calls.")
});
if !(wants_text || wants_finish || wants_tools) {
return Self::default();
}
let find = |name: &str| types.iter().find(|ty| ty.name == name && !ty.binding_excluded);
let Some(choice) = find(item)
.and_then(|chunk| field_type(chunk, "choices"))
.and_then(list_element)
.and_then(find)
else {
return Self::default();
};
let delta = field_type(choice, "delta").and_then(named).and_then(find);
let text = wants_text && delta.is_some_and(|delta| field_type(delta, "content").is_some_and(is_text));
let finish = wants_finish && field_type(choice, "finish_reason").is_some();
let tools = wants_tools
&& delta.is_some_and(|delta| field_type(delta, "tool_calls").is_some_and(|ty| list_element(ty).is_some()));
if !(text || finish || tools) {
return Self::default();
}
Self {
item: item.to_string(),
choice: choice.name.clone(),
delta: delta.map(|delta| delta.name.clone()).unwrap_or_default(),
text,
finish,
tools,
wire: WireKeys::resolve(choice, delta, types),
}
}
pub(super) fn render_declarations(&self, out: &mut String) {
if self.finish {
out.push_str(" char *stream_finish_reason = NULL;\n");
}
if self.tools {
out.push_str(" int stream_tool_calls_count = 0;\n");
out.push_str(" char *stream_tool_call_name = NULL;\n");
}
}
pub(super) fn render_release(&self, out: &mut String) {
if self.finish {
out.push_str(" free(stream_finish_reason);\n");
}
if self.tools {
out.push_str(" free(stream_tool_call_name);\n (void)stream_tool_calls_count;\n");
}
}
pub(super) fn render_capture(&self, out: &mut String, prefix: &str, result: &str, prefix_upper: &str) {
if !self.has_content() {
return;
}
let symbols =
|type_name: &str, field: &str| crate::codegen::naming::ffi_field_accessor_symbol(prefix, type_name, field);
out.push_str(&crate::e2e::template_env::render(
"c/stream_content_capture.jinja",
crate::alef_context! {
prefix => prefix, prefix_upper => prefix_upper, result => result,
text => self.text, finish => self.finish, tools => self.tools, wire => self.wire,
choices_getter => symbols(&self.item, "choices"),
choice_type => crate::codegen::naming::pascal_to_snake(&self.choice),
delta_getter => symbols(&self.choice, "delta"),
delta_type => crate::codegen::naming::pascal_to_snake(&self.delta),
content_getter => symbols(&self.delta, "content"),
},
));
}
pub(super) fn render_assertion(&self, out: &mut String, assertion: &Assertion) -> bool {
let Some(field) = assertion.field.as_deref() else {
return false;
};
let kind = assertion.assertion_type.as_str();
let text = assertion.value.as_ref().and_then(|value| value.as_str());
let count = assertion.value.as_ref().and_then(|value| value.as_u64());
let expression = match (field, kind) {
("stream_content", "equals") if self.text && text.is_some() => {
format!(
"strcmp(stream_content, \"{}\") == 0",
escape_c(text.unwrap_or_default())
)
}
("stream_content", "contains") if self.text && text.is_some() => {
format!(
"strstr(stream_content, \"{}\") != NULL",
escape_c(text.unwrap_or_default())
)
}
("stream_content", "not_empty") if self.text => "stream_content_len > 0".to_string(),
("finish_reason", "equals") if self.finish && text.is_some() => format!(
"stream_finish_reason != NULL && strcmp(stream_finish_reason, \"{}\") == 0",
escape_c(text.unwrap_or_default())
),
("finish_reason", "not_empty") if self.finish => "stream_finish_reason != NULL".to_string(),
("stream_complete", "is_true") if self.finish => "stream_finish_reason != NULL".to_string(),
("stream_complete", "is_false") if self.finish => "stream_finish_reason == NULL".to_string(),
("tool_calls", "not_empty") if self.tools => "stream_tool_calls_count > 0".to_string(),
("tool_calls", "count_equals") if self.tools && count.is_some() => {
format!("stream_tool_calls_count == {}", count.unwrap_or_default())
}
("tool_calls", "count_min") if self.tools && count.is_some() => {
format!("stream_tool_calls_count >= {}", count.unwrap_or_default())
}
("tool_calls[0].function.name", "equals") if self.tools && text.is_some() => format!(
"stream_tool_call_name != NULL && strcmp(stream_tool_call_name, \"{}\") == 0",
escape_c(text.unwrap_or_default())
),
_ => return false,
};
out.push_str(&crate::e2e::template_env::render(
"c/stream_content_assertion.jinja",
crate::alef_context! { expression => expression, kind => escape_c(kind), field => escape_c(field) },
));
true
}
}
fn escape_c(text: &str) -> String {
crate::e2e::escape::escape_c(text)
}
fn field_type<'a>(owner: &'a TypeDef, name: &str) -> Option<&'a TypeRef> {
crate::codegen::shared::binding_fields(&owner.fields)
.find(|field| field.name == name)
.map(|field| &field.ty)
}
fn named(ty: &TypeRef) -> Option<&str> {
match ty {
TypeRef::Named(name) => name.rsplit("::").next(),
TypeRef::Optional(inner) => named(inner),
_ => None,
}
}
fn list_element(ty: &TypeRef) -> Option<&str> {
match ty {
TypeRef::Vec(inner) => named(inner),
TypeRef::Optional(inner) => list_element(inner),
_ => None,
}
}
fn is_text(ty: &TypeRef) -> bool {
match ty {
TypeRef::String => true,
TypeRef::Optional(inner) => is_text(inner),
_ => false,
}
}
#[cfg(test)]
mod tests;