Skip to main content

foundry_local_sdk/
response.rs

1//! The [`Response`] value type produced by processing a [`Request`].
2//!
3//! [`Request`]: crate::Request
4
5use crate::detail::ffi::*;
6use crate::detail::session::NativeResponse;
7use crate::error::Result;
8use crate::item::Item;
9
10/// Why generation stopped for a [`Response`].
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub enum FinishReason {
13    /// No finish reason reported.
14    #[default]
15    None,
16    /// Generation ended due to an error.
17    Error,
18    /// The model emitted a stop condition (end-of-sequence / stop sequence).
19    Stop,
20    /// The maximum output length was reached.
21    Length,
22    /// The model requested one or more tool calls.
23    ToolCalls,
24}
25
26impl FinishReason {
27    pub(crate) fn from_native(value: flFinishReason) -> FinishReason {
28        match value {
29            FOUNDRY_LOCAL_FINISH_ERROR => FinishReason::Error,
30            FOUNDRY_LOCAL_FINISH_STOP => FinishReason::Stop,
31            FOUNDRY_LOCAL_FINISH_LENGTH => FinishReason::Length,
32            FOUNDRY_LOCAL_FINISH_TOOL_CALLS => FinishReason::ToolCalls,
33            _ => FinishReason::None,
34        }
35    }
36}
37
38/// Token accounting for a [`Response`].
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
40pub struct Usage {
41    /// Tokens consumed by the prompt / input.
42    pub prompt_tokens: u32,
43    /// Tokens produced in the completion / output.
44    pub completion_tokens: u32,
45    /// Total tokens (`prompt_tokens + completion_tokens`).
46    pub total_tokens: u32,
47}
48
49impl Usage {
50    /// Build usage from the native `(prompt, completion, total)` triple,
51    /// clamping any negative sentinel values to zero.
52    pub(crate) fn from_native(prompt: i64, completion: i64, total: i64) -> Usage {
53        Usage {
54            prompt_tokens: clamp_u32(prompt),
55            completion_tokens: clamp_u32(completion),
56            total_tokens: clamp_u32(total),
57        }
58    }
59}
60
61fn clamp_u32(v: i64) -> u32 {
62    v.clamp(0, u32::MAX as i64) as u32
63}
64
65/// The result of processing a [`Request`](crate::Request): output items plus the
66/// finish reason and token usage.
67#[derive(Debug, Clone, PartialEq)]
68pub struct Response {
69    /// The output items, in order.
70    pub items: Vec<Item>,
71    /// Why generation stopped.
72    pub finish_reason: FinishReason,
73    /// Token usage for the request.
74    pub usage: Usage,
75}
76
77impl Response {
78    /// Snapshot a native response into an owned [`Response`].
79    pub(crate) fn from_native(native: &NativeResponse) -> Result<Response> {
80        let items = native.items()?;
81        let finish_reason = FinishReason::from_native(native.finish_reason());
82        let (prompt, completion, total) = native.usage()?;
83        Ok(Response {
84            items,
85            finish_reason,
86            usage: Usage::from_native(prompt, completion, total),
87        })
88    }
89
90    /// The concatenated text of all textual output items.
91    ///
92    /// Handles both plain [`Item::Text`](crate::Item::Text) items and
93    /// [`Item::Message`](crate::Item::Message) items (whose text content parts are
94    /// concatenated); other item kinds are ignored. A convenience for chat
95    /// responses, where the output is typically a single assistant message.
96    pub fn text(&self) -> String {
97        let mut out = String::new();
98        for item in &self.items {
99            match item {
100                Item::Text { text, .. } => out.push_str(text),
101                Item::Message(message) => out.push_str(&message.text()),
102                _ => {}
103            }
104        }
105        out
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn finish_reason_from_native() {
115        assert_eq!(
116            FinishReason::from_native(FOUNDRY_LOCAL_FINISH_STOP),
117            FinishReason::Stop
118        );
119        assert_eq!(FinishReason::from_native(9999), FinishReason::None);
120    }
121
122    #[test]
123    fn usage_clamps_negatives() {
124        let u = Usage::from_native(-1, 5, i64::MAX);
125        assert_eq!(u.prompt_tokens, 0);
126        assert_eq!(u.completion_tokens, 5);
127        assert_eq!(u.total_tokens, u32::MAX);
128    }
129
130    #[test]
131    fn response_text_concatenates_text_and_message_items() {
132        let resp = Response {
133            items: vec![
134                Item::text("a"),
135                Item::bytes(vec![1]),
136                Item::text("b"),
137                Item::assistant_message(vec![Item::text("c")]),
138            ],
139            finish_reason: FinishReason::Stop,
140            usage: Usage::default(),
141        };
142        assert_eq!(resp.text(), "abc");
143    }
144}