use crate::detail::ffi::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ItemType {
Bytes,
Tensor,
Text,
Message,
Image,
Audio,
SpeechSegment,
SpeechResult,
ToolCall,
ToolResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextKind {
#[default]
Default,
Reasoning,
OpenAiJson,
}
impl TextKind {
pub(crate) fn to_native(self) -> flTextItemType {
match self {
TextKind::Default => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_DEFAULT,
TextKind::Reasoning => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING,
TextKind::OpenAiJson => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON,
}
}
pub(crate) fn from_native(value: flTextItemType) -> TextKind {
match value {
FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING => TextKind::Reasoning,
FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON => TextKind::OpenAiJson,
_ => TextKind::Default,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MessageRole {
#[default]
None,
System,
User,
Assistant,
Tool,
Developer,
}
impl MessageRole {
pub(crate) fn to_native(self) -> flMessageRole {
match self {
MessageRole::None => FOUNDRY_LOCAL_ROLE_NONE,
MessageRole::System => FOUNDRY_LOCAL_ROLE_SYSTEM,
MessageRole::User => FOUNDRY_LOCAL_ROLE_USER,
MessageRole::Assistant => FOUNDRY_LOCAL_ROLE_ASSISTANT,
MessageRole::Tool => FOUNDRY_LOCAL_ROLE_TOOL,
MessageRole::Developer => FOUNDRY_LOCAL_ROLE_DEVELOPER,
}
}
pub(crate) fn from_native(value: flMessageRole) -> MessageRole {
match value {
FOUNDRY_LOCAL_ROLE_SYSTEM => MessageRole::System,
FOUNDRY_LOCAL_ROLE_USER => MessageRole::User,
FOUNDRY_LOCAL_ROLE_ASSISTANT => MessageRole::Assistant,
FOUNDRY_LOCAL_ROLE_TOOL => MessageRole::Tool,
FOUNDRY_LOCAL_ROLE_DEVELOPER => MessageRole::Developer,
_ => MessageRole::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[allow(missing_docs)]
pub enum TensorDataType {
#[default]
Undefined,
Float,
Uint8,
Int8,
Uint16,
Int16,
Int32,
Int64,
String,
Bool,
Float16,
Double,
Uint32,
Uint64,
Complex64,
Complex128,
BFloat16,
Float8E4M3FN,
Float8E4M3FNUZ,
Float8E5M2,
Float8E5M2FNUZ,
Uint4,
Int4,
Float4E2M1,
Float8E8M0,
}
impl TensorDataType {
pub(crate) fn to_native(self) -> flTensorDataType {
match self {
TensorDataType::Undefined => FOUNDRY_LOCAL_TENSOR_UNDEFINED,
TensorDataType::Float => FOUNDRY_LOCAL_TENSOR_FLOAT,
TensorDataType::Uint8 => FOUNDRY_LOCAL_TENSOR_UINT8,
TensorDataType::Int8 => FOUNDRY_LOCAL_TENSOR_INT8,
TensorDataType::Uint16 => FOUNDRY_LOCAL_TENSOR_UINT16,
TensorDataType::Int16 => FOUNDRY_LOCAL_TENSOR_INT16,
TensorDataType::Int32 => FOUNDRY_LOCAL_TENSOR_INT32,
TensorDataType::Int64 => FOUNDRY_LOCAL_TENSOR_INT64,
TensorDataType::String => FOUNDRY_LOCAL_TENSOR_STRING,
TensorDataType::Bool => FOUNDRY_LOCAL_TENSOR_BOOL,
TensorDataType::Float16 => FOUNDRY_LOCAL_TENSOR_FLOAT16,
TensorDataType::Double => FOUNDRY_LOCAL_TENSOR_DOUBLE,
TensorDataType::Uint32 => FOUNDRY_LOCAL_TENSOR_UINT32,
TensorDataType::Uint64 => FOUNDRY_LOCAL_TENSOR_UINT64,
TensorDataType::Complex64 => FOUNDRY_LOCAL_TENSOR_COMPLEX64,
TensorDataType::Complex128 => FOUNDRY_LOCAL_TENSOR_COMPLEX128,
TensorDataType::BFloat16 => FOUNDRY_LOCAL_TENSOR_BFLOAT16,
TensorDataType::Float8E4M3FN => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN,
TensorDataType::Float8E4M3FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ,
TensorDataType::Float8E5M2 => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2,
TensorDataType::Float8E5M2FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ,
TensorDataType::Uint4 => FOUNDRY_LOCAL_TENSOR_UINT4,
TensorDataType::Int4 => FOUNDRY_LOCAL_TENSOR_INT4,
TensorDataType::Float4E2M1 => FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1,
TensorDataType::Float8E8M0 => FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0,
}
}
pub(crate) fn from_native(value: flTensorDataType) -> TensorDataType {
match value {
FOUNDRY_LOCAL_TENSOR_FLOAT => TensorDataType::Float,
FOUNDRY_LOCAL_TENSOR_UINT8 => TensorDataType::Uint8,
FOUNDRY_LOCAL_TENSOR_INT8 => TensorDataType::Int8,
FOUNDRY_LOCAL_TENSOR_UINT16 => TensorDataType::Uint16,
FOUNDRY_LOCAL_TENSOR_INT16 => TensorDataType::Int16,
FOUNDRY_LOCAL_TENSOR_INT32 => TensorDataType::Int32,
FOUNDRY_LOCAL_TENSOR_INT64 => TensorDataType::Int64,
FOUNDRY_LOCAL_TENSOR_STRING => TensorDataType::String,
FOUNDRY_LOCAL_TENSOR_BOOL => TensorDataType::Bool,
FOUNDRY_LOCAL_TENSOR_FLOAT16 => TensorDataType::Float16,
FOUNDRY_LOCAL_TENSOR_DOUBLE => TensorDataType::Double,
FOUNDRY_LOCAL_TENSOR_UINT32 => TensorDataType::Uint32,
FOUNDRY_LOCAL_TENSOR_UINT64 => TensorDataType::Uint64,
FOUNDRY_LOCAL_TENSOR_COMPLEX64 => TensorDataType::Complex64,
FOUNDRY_LOCAL_TENSOR_COMPLEX128 => TensorDataType::Complex128,
FOUNDRY_LOCAL_TENSOR_BFLOAT16 => TensorDataType::BFloat16,
FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN => TensorDataType::Float8E4M3FN,
FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ => TensorDataType::Float8E4M3FNUZ,
FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2 => TensorDataType::Float8E5M2,
FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ => TensorDataType::Float8E5M2FNUZ,
FOUNDRY_LOCAL_TENSOR_UINT4 => TensorDataType::Uint4,
FOUNDRY_LOCAL_TENSOR_INT4 => TensorDataType::Int4,
FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1 => TensorDataType::Float4E2M1,
FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0 => TensorDataType::Float8E8M0,
_ => TensorDataType::Undefined,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SpeechSegmentKind {
#[default]
None,
Partial,
Final,
}
impl SpeechSegmentKind {
pub(crate) fn from_native(value: flSpeechSegmentKind) -> SpeechSegmentKind {
match value {
FOUNDRY_LOCAL_SPEECH_SEGMENT_PARTIAL => SpeechSegmentKind::Partial,
FOUNDRY_LOCAL_SPEECH_SEGMENT_FINAL => SpeechSegmentKind::Final,
_ => SpeechSegmentKind::None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MediaSource {
Data(Vec<u8>),
Uri(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Message {
pub role: MessageRole,
pub content: Vec<Item>,
pub name: Option<String>,
}
impl Message {
pub fn new(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
Self {
role,
content: content.into(),
name: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn is_simple_text(&self) -> bool {
matches!(self.content.as_slice(), [Item::Text { .. }])
}
pub fn text(&self) -> String {
let mut out = String::new();
for part in &self.content {
if let Item::Text { text, .. } = part {
out.push_str(text);
}
}
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tensor {
pub data_type: TensorDataType,
pub shape: Vec<i64>,
pub data: Vec<u8>,
}
impl Tensor {
pub fn as_f32(&self) -> Option<Vec<f32>> {
if self.data_type != TensorDataType::Float || self.data.len() % 4 != 0 {
return None;
}
Some(
self.data
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Image {
pub source: MediaSource,
pub format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Audio {
pub source: MediaSource,
pub format: Option<String>,
pub sample_rate: i32,
pub channels: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolCall {
pub call_id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolResult {
pub call_id: String,
pub result: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpeechWord {
pub text: String,
pub start_time_ms: Option<i64>,
pub end_time_ms: Option<i64>,
pub confidence: Option<f32>,
pub speaker_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpeechSegment {
pub kind: SpeechSegmentKind,
pub text: String,
pub start_time_ms: Option<i64>,
pub end_time_ms: Option<i64>,
pub utterance_start: bool,
pub words: Vec<SpeechWord>,
pub language: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpeechResult {
pub text: String,
pub language: Option<String>,
pub duration_ms: Option<i64>,
pub segments: Vec<Item>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Item {
Text {
text: String,
kind: TextKind,
},
Message(Message),
Bytes(Vec<u8>),
Tensor(Tensor),
Image(Image),
Audio(Audio),
ToolCall(ToolCall),
ToolResult(ToolResult),
SpeechSegment(SpeechSegment),
SpeechResult(SpeechResult),
}
impl Item {
pub fn item_type(&self) -> ItemType {
match self {
Item::Text { .. } => ItemType::Text,
Item::Message(_) => ItemType::Message,
Item::Bytes(_) => ItemType::Bytes,
Item::Tensor(_) => ItemType::Tensor,
Item::Image(_) => ItemType::Image,
Item::Audio(_) => ItemType::Audio,
Item::ToolCall(_) => ItemType::ToolCall,
Item::ToolResult(_) => ItemType::ToolResult,
Item::SpeechSegment(_) => ItemType::SpeechSegment,
Item::SpeechResult(_) => ItemType::SpeechResult,
}
}
pub fn text(text: impl Into<String>) -> Self {
Item::Text {
text: text.into(),
kind: TextKind::Default,
}
}
pub fn reasoning(text: impl Into<String>) -> Self {
Item::Text {
text: text.into(),
kind: TextKind::Reasoning,
}
}
pub fn message(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
Item::Message(Message::new(role, content))
}
pub fn system_message(content: impl Into<Vec<Item>>) -> Self {
Item::message(MessageRole::System, content)
}
pub fn user_message(content: impl Into<Vec<Item>>) -> Self {
Item::message(MessageRole::User, content)
}
pub fn assistant_message(content: impl Into<Vec<Item>>) -> Self {
Item::message(MessageRole::Assistant, content)
}
pub fn developer_message(content: impl Into<Vec<Item>>) -> Self {
Item::message(MessageRole::Developer, content)
}
pub fn tool_message(content: impl Into<Vec<Item>>) -> Self {
Item::message(MessageRole::Tool, content)
}
pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
Item::Bytes(data.into())
}
pub fn tensor(
data_type: TensorDataType,
shape: impl Into<Vec<i64>>,
data: impl Into<Vec<u8>>,
) -> Self {
Item::Tensor(Tensor {
data_type,
shape: shape.into(),
data: data.into(),
})
}
pub fn float_tensor(shape: impl Into<Vec<i64>>, data: &[f32]) -> Self {
let mut bytes = Vec::with_capacity(data.len() * 4);
for f in data {
bytes.extend_from_slice(&f.to_le_bytes());
}
Item::Tensor(Tensor {
data_type: TensorDataType::Float,
shape: shape.into(),
data: bytes,
})
}
pub fn image_data(data: impl Into<Vec<u8>>, format: Option<impl Into<String>>) -> Self {
Item::Image(Image {
source: MediaSource::Data(data.into()),
format: format.map(Into::into),
})
}
pub fn image_uri(uri: impl Into<String>, format: Option<impl Into<String>>) -> Self {
Item::Image(Image {
source: MediaSource::Uri(uri.into()),
format: format.map(Into::into),
})
}
pub fn audio_data(
data: impl Into<Vec<u8>>,
format: Option<impl Into<String>>,
sample_rate: i32,
channels: i32,
) -> Self {
Item::Audio(Audio {
source: MediaSource::Data(data.into()),
format: format.map(Into::into),
sample_rate,
channels,
})
}
pub fn audio_uri(
uri: impl Into<String>,
format: Option<impl Into<String>>,
sample_rate: i32,
channels: i32,
) -> Self {
Item::Audio(Audio {
source: MediaSource::Uri(uri.into()),
format: format.map(Into::into),
sample_rate,
channels,
})
}
pub fn tool_call(
call_id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Item::ToolCall(ToolCall {
call_id: call_id.into(),
name: name.into(),
arguments: arguments.into(),
})
}
pub fn tool_result(call_id: impl Into<String>, result: impl Into<String>) -> Self {
Item::ToolResult(ToolResult {
call_id: call_id.into(),
result: result.into(),
})
}
pub fn as_text(&self) -> Option<&str> {
match self {
Item::Text { text, .. } => Some(text),
_ => None,
}
}
pub fn as_message(&self) -> Option<&Message> {
match self {
Item::Message(m) => Some(m),
_ => None,
}
}
pub fn as_tensor(&self) -> Option<&Tensor> {
match self {
Item::Tensor(t) => Some(t),
_ => None,
}
}
pub fn as_tool_call(&self) -> Option<&ToolCall> {
match self {
Item::ToolCall(c) => Some(c),
_ => None,
}
}
pub fn as_speech_result(&self) -> Option<&SpeechResult> {
match self {
Item::SpeechResult(r) => Some(r),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_type_matches_variant() {
assert_eq!(Item::text("hi").item_type(), ItemType::Text);
assert_eq!(Item::bytes(vec![1, 2]).item_type(), ItemType::Bytes);
assert_eq!(
Item::tool_call("c", "f", "{}").item_type(),
ItemType::ToolCall
);
}
#[test]
fn message_helpers() {
let m = Item::user_message(vec![Item::text("hello"), Item::text(" world")]);
let msg = m.as_message().unwrap();
assert_eq!(msg.role, MessageRole::User);
assert!(!msg.is_simple_text());
assert_eq!(msg.text(), "hello world");
let simple = Message::new(MessageRole::System, vec![Item::text("x")]);
assert!(simple.is_simple_text());
}
#[test]
fn float_tensor_round_trips_bytes() {
let values = [1.0f32, -2.5, 3.25];
let item = Item::float_tensor(vec![3], &values);
let t = item.as_tensor().unwrap();
assert_eq!(t.data_type, TensorDataType::Float);
assert_eq!(t.shape, vec![3]);
assert_eq!(t.as_f32().unwrap(), values);
}
#[test]
fn native_enum_mappings_round_trip() {
for kind in [TextKind::Default, TextKind::Reasoning, TextKind::OpenAiJson] {
assert_eq!(TextKind::from_native(kind.to_native()), kind);
}
for role in [
MessageRole::None,
MessageRole::System,
MessageRole::User,
MessageRole::Assistant,
MessageRole::Tool,
MessageRole::Developer,
] {
assert_eq!(MessageRole::from_native(role.to_native()), role);
}
for dt in [
TensorDataType::Undefined,
TensorDataType::Float,
TensorDataType::Int64,
TensorDataType::BFloat16,
TensorDataType::Float8E8M0,
] {
assert_eq!(TensorDataType::from_native(dt.to_native()), dt);
}
}
}