Skip to main content

foundry_local_sdk/
item.rs

1//! The [`Item`] discriminated union and its supporting value types.
2//!
3//! An [`Item`] is the unit of data exchanged with a [`Session`](crate::Session):
4//! requests carry input items (text, messages, images, audio, tensors, tool
5//! calls/results) and responses carry output items (text, tensors, tool calls,
6//! speech results, …).
7//!
8//! Unlike the class hierarchies used by the C++/C#/Python SDKs, the Rust surface
9//! models items as a plain, owned `enum`. Items are pure data: they hold no
10//! native handle, are `Send + Sync + Clone`, and can be constructed, matched,
11//! and inspected without a loaded native library. Conversion to and from the
12//! native `flItem` representation happens transiently inside the session when a
13//! request is processed (see [`crate::detail::items`]).
14
15use crate::detail::ffi::*;
16
17/// The kind of payload carried by an [`Item`].
18///
19/// Mirrors the native `flItemType` discriminant and is returned by
20/// [`Item::item_type`].
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum ItemType {
23    /// Opaque byte buffer ([`Item::Bytes`]).
24    Bytes,
25    /// Numeric tensor ([`Item::Tensor`]).
26    Tensor,
27    /// UTF-8 text ([`Item::Text`]).
28    Text,
29    /// Chat message with nested content parts ([`Item::Message`]).
30    Message,
31    /// Image, inline or by URI ([`Item::Image`]).
32    Image,
33    /// Audio, inline or by URI ([`Item::Audio`]).
34    Audio,
35    /// A single speech-recognition segment ([`Item::SpeechSegment`]).
36    SpeechSegment,
37    /// A complete speech-recognition result ([`Item::SpeechResult`]).
38    SpeechResult,
39    /// A model-issued tool/function call ([`Item::ToolCall`]).
40    ToolCall,
41    /// The result of executing a tool/function call ([`Item::ToolResult`]).
42    ToolResult,
43}
44
45/// The subtype of a [`Item::Text`] payload.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47pub enum TextKind {
48    /// Ordinary user- or model-visible text.
49    #[default]
50    Default,
51    /// Model reasoning / chain-of-thought text.
52    Reasoning,
53    /// An opaque OpenAI-compatible REST JSON payload (used by the higher-level
54    /// OpenAI facade). Rarely constructed directly.
55    OpenAiJson,
56}
57
58impl TextKind {
59    pub(crate) fn to_native(self) -> flTextItemType {
60        match self {
61            TextKind::Default => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_DEFAULT,
62            TextKind::Reasoning => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING,
63            TextKind::OpenAiJson => FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON,
64        }
65    }
66
67    pub(crate) fn from_native(value: flTextItemType) -> TextKind {
68        match value {
69            FOUNDRY_LOCAL_TEXT_ITEM_TYPE_REASONING => TextKind::Reasoning,
70            FOUNDRY_LOCAL_TEXT_ITEM_TYPE_OPENAI_JSON => TextKind::OpenAiJson,
71            _ => TextKind::Default,
72        }
73    }
74}
75
76/// The author role of a chat [`Message`].
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum MessageRole {
79    /// No role specified (only seen when reading messages that omit a role).
80    #[default]
81    None,
82    /// System / instruction message.
83    System,
84    /// End-user message.
85    User,
86    /// Model / assistant message.
87    Assistant,
88    /// Tool / function output message.
89    Tool,
90    /// Developer message (higher-priority instructions than `System`).
91    Developer,
92}
93
94impl MessageRole {
95    pub(crate) fn to_native(self) -> flMessageRole {
96        match self {
97            MessageRole::None => FOUNDRY_LOCAL_ROLE_NONE,
98            MessageRole::System => FOUNDRY_LOCAL_ROLE_SYSTEM,
99            MessageRole::User => FOUNDRY_LOCAL_ROLE_USER,
100            MessageRole::Assistant => FOUNDRY_LOCAL_ROLE_ASSISTANT,
101            MessageRole::Tool => FOUNDRY_LOCAL_ROLE_TOOL,
102            MessageRole::Developer => FOUNDRY_LOCAL_ROLE_DEVELOPER,
103        }
104    }
105
106    pub(crate) fn from_native(value: flMessageRole) -> MessageRole {
107        match value {
108            FOUNDRY_LOCAL_ROLE_SYSTEM => MessageRole::System,
109            FOUNDRY_LOCAL_ROLE_USER => MessageRole::User,
110            FOUNDRY_LOCAL_ROLE_ASSISTANT => MessageRole::Assistant,
111            FOUNDRY_LOCAL_ROLE_TOOL => MessageRole::Tool,
112            FOUNDRY_LOCAL_ROLE_DEVELOPER => MessageRole::Developer,
113            _ => MessageRole::None,
114        }
115    }
116}
117
118/// The element data type of a [`Tensor`], mirroring ONNX tensor element types.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
120#[allow(missing_docs)]
121pub enum TensorDataType {
122    #[default]
123    Undefined,
124    Float,
125    Uint8,
126    Int8,
127    Uint16,
128    Int16,
129    Int32,
130    Int64,
131    String,
132    Bool,
133    Float16,
134    Double,
135    Uint32,
136    Uint64,
137    Complex64,
138    Complex128,
139    BFloat16,
140    Float8E4M3FN,
141    Float8E4M3FNUZ,
142    Float8E5M2,
143    Float8E5M2FNUZ,
144    Uint4,
145    Int4,
146    Float4E2M1,
147    Float8E8M0,
148}
149
150impl TensorDataType {
151    pub(crate) fn to_native(self) -> flTensorDataType {
152        match self {
153            TensorDataType::Undefined => FOUNDRY_LOCAL_TENSOR_UNDEFINED,
154            TensorDataType::Float => FOUNDRY_LOCAL_TENSOR_FLOAT,
155            TensorDataType::Uint8 => FOUNDRY_LOCAL_TENSOR_UINT8,
156            TensorDataType::Int8 => FOUNDRY_LOCAL_TENSOR_INT8,
157            TensorDataType::Uint16 => FOUNDRY_LOCAL_TENSOR_UINT16,
158            TensorDataType::Int16 => FOUNDRY_LOCAL_TENSOR_INT16,
159            TensorDataType::Int32 => FOUNDRY_LOCAL_TENSOR_INT32,
160            TensorDataType::Int64 => FOUNDRY_LOCAL_TENSOR_INT64,
161            TensorDataType::String => FOUNDRY_LOCAL_TENSOR_STRING,
162            TensorDataType::Bool => FOUNDRY_LOCAL_TENSOR_BOOL,
163            TensorDataType::Float16 => FOUNDRY_LOCAL_TENSOR_FLOAT16,
164            TensorDataType::Double => FOUNDRY_LOCAL_TENSOR_DOUBLE,
165            TensorDataType::Uint32 => FOUNDRY_LOCAL_TENSOR_UINT32,
166            TensorDataType::Uint64 => FOUNDRY_LOCAL_TENSOR_UINT64,
167            TensorDataType::Complex64 => FOUNDRY_LOCAL_TENSOR_COMPLEX64,
168            TensorDataType::Complex128 => FOUNDRY_LOCAL_TENSOR_COMPLEX128,
169            TensorDataType::BFloat16 => FOUNDRY_LOCAL_TENSOR_BFLOAT16,
170            TensorDataType::Float8E4M3FN => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN,
171            TensorDataType::Float8E4M3FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ,
172            TensorDataType::Float8E5M2 => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2,
173            TensorDataType::Float8E5M2FNUZ => FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ,
174            TensorDataType::Uint4 => FOUNDRY_LOCAL_TENSOR_UINT4,
175            TensorDataType::Int4 => FOUNDRY_LOCAL_TENSOR_INT4,
176            TensorDataType::Float4E2M1 => FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1,
177            TensorDataType::Float8E8M0 => FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0,
178        }
179    }
180
181    pub(crate) fn from_native(value: flTensorDataType) -> TensorDataType {
182        match value {
183            FOUNDRY_LOCAL_TENSOR_FLOAT => TensorDataType::Float,
184            FOUNDRY_LOCAL_TENSOR_UINT8 => TensorDataType::Uint8,
185            FOUNDRY_LOCAL_TENSOR_INT8 => TensorDataType::Int8,
186            FOUNDRY_LOCAL_TENSOR_UINT16 => TensorDataType::Uint16,
187            FOUNDRY_LOCAL_TENSOR_INT16 => TensorDataType::Int16,
188            FOUNDRY_LOCAL_TENSOR_INT32 => TensorDataType::Int32,
189            FOUNDRY_LOCAL_TENSOR_INT64 => TensorDataType::Int64,
190            FOUNDRY_LOCAL_TENSOR_STRING => TensorDataType::String,
191            FOUNDRY_LOCAL_TENSOR_BOOL => TensorDataType::Bool,
192            FOUNDRY_LOCAL_TENSOR_FLOAT16 => TensorDataType::Float16,
193            FOUNDRY_LOCAL_TENSOR_DOUBLE => TensorDataType::Double,
194            FOUNDRY_LOCAL_TENSOR_UINT32 => TensorDataType::Uint32,
195            FOUNDRY_LOCAL_TENSOR_UINT64 => TensorDataType::Uint64,
196            FOUNDRY_LOCAL_TENSOR_COMPLEX64 => TensorDataType::Complex64,
197            FOUNDRY_LOCAL_TENSOR_COMPLEX128 => TensorDataType::Complex128,
198            FOUNDRY_LOCAL_TENSOR_BFLOAT16 => TensorDataType::BFloat16,
199            FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FN => TensorDataType::Float8E4M3FN,
200            FOUNDRY_LOCAL_TENSOR_FLOAT8E4M3FNUZ => TensorDataType::Float8E4M3FNUZ,
201            FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2 => TensorDataType::Float8E5M2,
202            FOUNDRY_LOCAL_TENSOR_FLOAT8E5M2FNUZ => TensorDataType::Float8E5M2FNUZ,
203            FOUNDRY_LOCAL_TENSOR_UINT4 => TensorDataType::Uint4,
204            FOUNDRY_LOCAL_TENSOR_INT4 => TensorDataType::Int4,
205            FOUNDRY_LOCAL_TENSOR_FLOAT4E2M1 => TensorDataType::Float4E2M1,
206            FOUNDRY_LOCAL_TENSOR_FLOAT8E8M0 => TensorDataType::Float8E8M0,
207            _ => TensorDataType::Undefined,
208        }
209    }
210}
211
212/// Whether a [`SpeechSegment`] is an interim hypothesis or a stable result.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
214pub enum SpeechSegmentKind {
215    /// Unspecified.
216    #[default]
217    None,
218    /// An interim hypothesis that may still change.
219    Partial,
220    /// A stabilized, final segment.
221    Final,
222}
223
224impl SpeechSegmentKind {
225    pub(crate) fn from_native(value: flSpeechSegmentKind) -> SpeechSegmentKind {
226        match value {
227            FOUNDRY_LOCAL_SPEECH_SEGMENT_PARTIAL => SpeechSegmentKind::Partial,
228            FOUNDRY_LOCAL_SPEECH_SEGMENT_FINAL => SpeechSegmentKind::Final,
229            _ => SpeechSegmentKind::None,
230        }
231    }
232}
233
234/// The source of an [`Image`] or [`Audio`] payload: inline bytes or a URI.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum MediaSource {
237    /// Inline, raw bytes.
238    Data(Vec<u8>),
239    /// A reference to external content by URI.
240    Uri(String),
241}
242
243/// A chat message: an author [`role`](MessageRole) plus ordered content parts.
244///
245/// Content parts are themselves [`Item`]s, allowing multimodal messages (e.g. a
246/// [`Item::Text`] alongside a [`Item::Image`]).
247#[derive(Debug, Clone, PartialEq)]
248pub struct Message {
249    /// The author role.
250    pub role: MessageRole,
251    /// The ordered content parts of the message.
252    pub content: Vec<Item>,
253    /// Optional participant name.
254    pub name: Option<String>,
255}
256
257impl Message {
258    /// Create a message with the given role and content parts.
259    pub fn new(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
260        Self {
261            role,
262            content: content.into(),
263            name: None,
264        }
265    }
266
267    /// Set the participant name (builder-style).
268    pub fn with_name(mut self, name: impl Into<String>) -> Self {
269        self.name = Some(name.into());
270        self
271    }
272
273    /// Whether the message consists of a single [`Item::Text`] content part.
274    pub fn is_simple_text(&self) -> bool {
275        matches!(self.content.as_slice(), [Item::Text { .. }])
276    }
277
278    /// The concatenated text of all [`Item::Text`] content parts.
279    ///
280    /// Non-text parts are ignored. Returns an empty string if there are none.
281    pub fn text(&self) -> String {
282        let mut out = String::new();
283        for part in &self.content {
284            if let Item::Text { text, .. } = part {
285                out.push_str(text);
286            }
287        }
288        out
289    }
290}
291
292/// A numeric tensor: an element [`data_type`](TensorDataType), a `shape`, and the
293/// raw little-endian element bytes.
294#[derive(Debug, Clone, PartialEq)]
295pub struct Tensor {
296    /// The element data type.
297    pub data_type: TensorDataType,
298    /// The dimensions of the tensor.
299    pub shape: Vec<i64>,
300    /// The raw element bytes, row-major, little-endian.
301    pub data: Vec<u8>,
302}
303
304impl Tensor {
305    /// Interpret the raw bytes as `f32` elements.
306    ///
307    /// Returns `None` unless [`data_type`](Self::data_type) is
308    /// [`TensorDataType::Float`] and the byte length is a multiple of four.
309    pub fn as_f32(&self) -> Option<Vec<f32>> {
310        if self.data_type != TensorDataType::Float || self.data.len() % 4 != 0 {
311            return None;
312        }
313        Some(
314            self.data
315                .chunks_exact(4)
316                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
317                .collect(),
318        )
319    }
320}
321
322/// An image payload, inline or by URI, with an optional `format` hint (e.g. `png`).
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct Image {
325    /// The image content.
326    pub source: MediaSource,
327    /// Optional format hint (e.g. `"png"`, `"jpeg"`).
328    pub format: Option<String>,
329}
330
331/// An audio payload, inline or by URI, with format and PCM layout hints.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct Audio {
334    /// The audio content.
335    pub source: MediaSource,
336    /// Optional format hint (e.g. `"wav"`, `"pcm16"`).
337    pub format: Option<String>,
338    /// Sample rate in Hz, or `0` if unspecified.
339    pub sample_rate: i32,
340    /// Channel count, or `0` if unspecified.
341    pub channels: i32,
342}
343
344/// A model-issued request to invoke a named tool/function.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct ToolCall {
347    /// Correlates the call with its [`ToolResult`].
348    pub call_id: String,
349    /// The name of the tool/function to invoke.
350    pub name: String,
351    /// The call arguments, as a JSON string.
352    pub arguments: String,
353}
354
355/// The result of executing a [`ToolCall`], fed back to the model.
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct ToolResult {
358    /// The `call_id` of the originating [`ToolCall`].
359    pub call_id: String,
360    /// The tool output, typically a JSON or text string.
361    pub result: String,
362}
363
364/// One recognized word within a [`SpeechSegment`] (output-only).
365#[derive(Debug, Clone, PartialEq)]
366pub struct SpeechWord {
367    /// The word text.
368    pub text: String,
369    /// Start offset from the beginning of the audio, in milliseconds.
370    pub start_time_ms: Option<i64>,
371    /// End offset from the beginning of the audio, in milliseconds.
372    pub end_time_ms: Option<i64>,
373    /// Recognition confidence in `[0, 1]`, if reported.
374    pub confidence: Option<f32>,
375    /// Diarization speaker id, if reported.
376    pub speaker_id: Option<String>,
377}
378
379/// One speech-recognition segment (output-only).
380#[derive(Debug, Clone, PartialEq)]
381pub struct SpeechSegment {
382    /// Whether this is an interim or final segment.
383    pub kind: SpeechSegmentKind,
384    /// The recognized text of the segment.
385    pub text: String,
386    /// Start offset from the beginning of the audio, in milliseconds.
387    pub start_time_ms: Option<i64>,
388    /// End offset from the beginning of the audio, in milliseconds.
389    pub end_time_ms: Option<i64>,
390    /// Whether this segment starts a new utterance.
391    pub utterance_start: bool,
392    /// Per-word timing/confidence, when available.
393    pub words: Vec<SpeechWord>,
394    /// Detected language (e.g. `"en"`), if reported.
395    pub language: Option<String>,
396}
397
398/// A complete speech-recognition result (output-only).
399#[derive(Debug, Clone, PartialEq)]
400pub struct SpeechResult {
401    /// The full concatenated transcript.
402    pub text: String,
403    /// Detected language (e.g. `"en"`), if reported.
404    pub language: Option<String>,
405    /// Total audio duration in milliseconds, if reported.
406    pub duration_ms: Option<i64>,
407    /// The constituent segments, each a [`Item::SpeechSegment`].
408    pub segments: Vec<Item>,
409}
410
411/// A single unit of input or output data exchanged with a [`Session`](crate::Session).
412///
413/// `Item` is a pure-data, owned value: it holds no native handle and is
414/// `Send + Sync + Clone`. Construct items with the associated functions (e.g.
415/// [`Item::text`], [`Item::user_message`], [`Item::image_data`]) or the struct
416/// variants directly, and inspect them by pattern matching.
417#[derive(Debug, Clone, PartialEq)]
418pub enum Item {
419    /// UTF-8 text of a given [`TextKind`].
420    Text {
421        /// The text content.
422        text: String,
423        /// The text subtype.
424        kind: TextKind,
425    },
426    /// A chat message with nested content parts.
427    Message(Message),
428    /// An opaque byte buffer.
429    Bytes(Vec<u8>),
430    /// A numeric tensor (e.g. an embedding vector).
431    Tensor(Tensor),
432    /// An image, inline or by URI.
433    Image(Image),
434    /// An audio clip, inline or by URI.
435    Audio(Audio),
436    /// A model-issued tool/function call.
437    ToolCall(ToolCall),
438    /// The result of executing a tool/function call.
439    ToolResult(ToolResult),
440    /// A speech-recognition segment (output-only).
441    SpeechSegment(SpeechSegment),
442    /// A speech-recognition result (output-only).
443    SpeechResult(SpeechResult),
444}
445
446impl Item {
447    /// The [`ItemType`] discriminant of this item.
448    pub fn item_type(&self) -> ItemType {
449        match self {
450            Item::Text { .. } => ItemType::Text,
451            Item::Message(_) => ItemType::Message,
452            Item::Bytes(_) => ItemType::Bytes,
453            Item::Tensor(_) => ItemType::Tensor,
454            Item::Image(_) => ItemType::Image,
455            Item::Audio(_) => ItemType::Audio,
456            Item::ToolCall(_) => ItemType::ToolCall,
457            Item::ToolResult(_) => ItemType::ToolResult,
458            Item::SpeechSegment(_) => ItemType::SpeechSegment,
459            Item::SpeechResult(_) => ItemType::SpeechResult,
460        }
461    }
462
463    // ── Text constructors ────────────────────────────────────────────────────
464
465    /// A default-kind [`Item::Text`].
466    pub fn text(text: impl Into<String>) -> Self {
467        Item::Text {
468            text: text.into(),
469            kind: TextKind::Default,
470        }
471    }
472
473    /// A reasoning / chain-of-thought [`Item::Text`].
474    pub fn reasoning(text: impl Into<String>) -> Self {
475        Item::Text {
476            text: text.into(),
477            kind: TextKind::Reasoning,
478        }
479    }
480
481    // ── Message constructors ─────────────────────────────────────────────────
482
483    /// A [`Item::Message`] with the given role and content parts.
484    pub fn message(role: MessageRole, content: impl Into<Vec<Item>>) -> Self {
485        Item::Message(Message::new(role, content))
486    }
487
488    /// A `system` [`Item::Message`].
489    pub fn system_message(content: impl Into<Vec<Item>>) -> Self {
490        Item::message(MessageRole::System, content)
491    }
492
493    /// A `user` [`Item::Message`].
494    pub fn user_message(content: impl Into<Vec<Item>>) -> Self {
495        Item::message(MessageRole::User, content)
496    }
497
498    /// An `assistant` [`Item::Message`].
499    pub fn assistant_message(content: impl Into<Vec<Item>>) -> Self {
500        Item::message(MessageRole::Assistant, content)
501    }
502
503    /// A `developer` [`Item::Message`].
504    pub fn developer_message(content: impl Into<Vec<Item>>) -> Self {
505        Item::message(MessageRole::Developer, content)
506    }
507
508    /// A `tool` [`Item::Message`].
509    pub fn tool_message(content: impl Into<Vec<Item>>) -> Self {
510        Item::message(MessageRole::Tool, content)
511    }
512
513    // ── Binary / tensor constructors ─────────────────────────────────────────
514
515    /// An [`Item::Bytes`] carrying an opaque byte buffer.
516    pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
517        Item::Bytes(data.into())
518    }
519
520    /// An [`Item::Tensor`] from a data type, shape, and raw element bytes.
521    pub fn tensor(
522        data_type: TensorDataType,
523        shape: impl Into<Vec<i64>>,
524        data: impl Into<Vec<u8>>,
525    ) -> Self {
526        Item::Tensor(Tensor {
527            data_type,
528            shape: shape.into(),
529            data: data.into(),
530        })
531    }
532
533    /// A [`TensorDataType::Float`] [`Item::Tensor`] from `f32` elements.
534    pub fn float_tensor(shape: impl Into<Vec<i64>>, data: &[f32]) -> Self {
535        let mut bytes = Vec::with_capacity(data.len() * 4);
536        for f in data {
537            bytes.extend_from_slice(&f.to_le_bytes());
538        }
539        Item::Tensor(Tensor {
540            data_type: TensorDataType::Float,
541            shape: shape.into(),
542            data: bytes,
543        })
544    }
545
546    // ── Image / audio constructors ───────────────────────────────────────────
547
548    /// An [`Item::Image`] from inline bytes and an optional format hint.
549    pub fn image_data(data: impl Into<Vec<u8>>, format: Option<impl Into<String>>) -> Self {
550        Item::Image(Image {
551            source: MediaSource::Data(data.into()),
552            format: format.map(Into::into),
553        })
554    }
555
556    /// An [`Item::Image`] referencing external content by URI.
557    pub fn image_uri(uri: impl Into<String>, format: Option<impl Into<String>>) -> Self {
558        Item::Image(Image {
559            source: MediaSource::Uri(uri.into()),
560            format: format.map(Into::into),
561        })
562    }
563
564    /// An [`Item::Audio`] from inline bytes, format hint, and PCM layout.
565    pub fn audio_data(
566        data: impl Into<Vec<u8>>,
567        format: Option<impl Into<String>>,
568        sample_rate: i32,
569        channels: i32,
570    ) -> Self {
571        Item::Audio(Audio {
572            source: MediaSource::Data(data.into()),
573            format: format.map(Into::into),
574            sample_rate,
575            channels,
576        })
577    }
578
579    /// An [`Item::Audio`] referencing external content by URI.
580    pub fn audio_uri(
581        uri: impl Into<String>,
582        format: Option<impl Into<String>>,
583        sample_rate: i32,
584        channels: i32,
585    ) -> Self {
586        Item::Audio(Audio {
587            source: MediaSource::Uri(uri.into()),
588            format: format.map(Into::into),
589            sample_rate,
590            channels,
591        })
592    }
593
594    // ── Tool constructors ────────────────────────────────────────────────────
595
596    /// A [`Item::ToolCall`].
597    pub fn tool_call(
598        call_id: impl Into<String>,
599        name: impl Into<String>,
600        arguments: impl Into<String>,
601    ) -> Self {
602        Item::ToolCall(ToolCall {
603            call_id: call_id.into(),
604            name: name.into(),
605            arguments: arguments.into(),
606        })
607    }
608
609    /// A [`Item::ToolResult`].
610    pub fn tool_result(call_id: impl Into<String>, result: impl Into<String>) -> Self {
611        Item::ToolResult(ToolResult {
612            call_id: call_id.into(),
613            result: result.into(),
614        })
615    }
616
617    // ── Accessors ────────────────────────────────────────────────────────────
618
619    /// The text content, if this is a [`Item::Text`].
620    pub fn as_text(&self) -> Option<&str> {
621        match self {
622            Item::Text { text, .. } => Some(text),
623            _ => None,
624        }
625    }
626
627    /// The [`Message`], if this is a [`Item::Message`].
628    pub fn as_message(&self) -> Option<&Message> {
629        match self {
630            Item::Message(m) => Some(m),
631            _ => None,
632        }
633    }
634
635    /// The [`Tensor`], if this is a [`Item::Tensor`].
636    pub fn as_tensor(&self) -> Option<&Tensor> {
637        match self {
638            Item::Tensor(t) => Some(t),
639            _ => None,
640        }
641    }
642
643    /// The [`ToolCall`], if this is a [`Item::ToolCall`].
644    pub fn as_tool_call(&self) -> Option<&ToolCall> {
645        match self {
646            Item::ToolCall(c) => Some(c),
647            _ => None,
648        }
649    }
650
651    /// The [`SpeechResult`], if this is a [`Item::SpeechResult`].
652    pub fn as_speech_result(&self) -> Option<&SpeechResult> {
653        match self {
654            Item::SpeechResult(r) => Some(r),
655            _ => None,
656        }
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn item_type_matches_variant() {
666        assert_eq!(Item::text("hi").item_type(), ItemType::Text);
667        assert_eq!(Item::bytes(vec![1, 2]).item_type(), ItemType::Bytes);
668        assert_eq!(
669            Item::tool_call("c", "f", "{}").item_type(),
670            ItemType::ToolCall
671        );
672    }
673
674    #[test]
675    fn message_helpers() {
676        let m = Item::user_message(vec![Item::text("hello"), Item::text(" world")]);
677        let msg = m.as_message().unwrap();
678        assert_eq!(msg.role, MessageRole::User);
679        assert!(!msg.is_simple_text());
680        assert_eq!(msg.text(), "hello world");
681
682        let simple = Message::new(MessageRole::System, vec![Item::text("x")]);
683        assert!(simple.is_simple_text());
684    }
685
686    #[test]
687    fn float_tensor_round_trips_bytes() {
688        let values = [1.0f32, -2.5, 3.25];
689        let item = Item::float_tensor(vec![3], &values);
690        let t = item.as_tensor().unwrap();
691        assert_eq!(t.data_type, TensorDataType::Float);
692        assert_eq!(t.shape, vec![3]);
693        assert_eq!(t.as_f32().unwrap(), values);
694    }
695
696    #[test]
697    fn native_enum_mappings_round_trip() {
698        for kind in [TextKind::Default, TextKind::Reasoning, TextKind::OpenAiJson] {
699            assert_eq!(TextKind::from_native(kind.to_native()), kind);
700        }
701        for role in [
702            MessageRole::None,
703            MessageRole::System,
704            MessageRole::User,
705            MessageRole::Assistant,
706            MessageRole::Tool,
707            MessageRole::Developer,
708        ] {
709            assert_eq!(MessageRole::from_native(role.to_native()), role);
710        }
711        for dt in [
712            TensorDataType::Undefined,
713            TensorDataType::Float,
714            TensorDataType::Int64,
715            TensorDataType::BFloat16,
716            TensorDataType::Float8E8M0,
717        ] {
718            assert_eq!(TensorDataType::from_native(dt.to_native()), dt);
719        }
720    }
721}