Skip to main content

ferrin_core/output/
mod.rs

1//! Structured output strategies for `generate_text` and `stream_text`.
2//!
3//! An [`Output`] decides the response format sent to the model and parses
4//! the final (and, when streaming, partial) text.
5
6use std::fmt;
7use std::sync::Arc;
8
9use ferrin_schema::JsonSchema;
10use ferrin_schema::Schema;
11use ferrin_spec::FinishReason;
12use ferrin_spec::JsonValue;
13use ferrin_spec::ResponseFormat;
14use ferrin_spec::ResponseMetadata;
15use ferrin_spec::Usage;
16use serde::de::DeserializeOwned;
17
18use crate::error::Error;
19
20mod strategies;
21
22pub use strategies::ArrayOutput;
23pub use strategies::ChoiceOutput;
24pub use strategies::JsonOutput;
25pub use strategies::ObjectOutput;
26pub use strategies::TextOutput;
27
28/// Response context handed to [`OutputHandler::parse_complete`] (used to
29/// populate `NoObjectGenerated` errors).
30#[derive(Debug, Clone, PartialEq)]
31pub struct OutputContext {
32    /// Response metadata of the final step.
33    pub response: ResponseMetadata,
34    /// Usage of the final step.
35    pub usage: Usage,
36    /// Finish reason of the final step.
37    pub finish_reason: FinishReason,
38}
39
40/// Implements an output strategy for output type `O`.
41///
42/// Implement this to add custom strategies; the built-in ones are exposed
43/// through [`Output`].
44pub trait OutputHandler<O>: Send + Sync + 'static {
45    /// The response format sent to the model.
46    fn response_format(&self) -> Option<ResponseFormat>;
47
48    /// Whether the call must produce an output (`false` only for [`NoOutput`]).
49    fn wants_output(&self) -> bool {
50        true
51    }
52
53    /// Parses the complete text of the final step.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`Error::NoObjectGenerated`] when the text does not satisfy the
58    /// strategy.
59    fn parse_complete(&self, text: &str, ctx: &OutputContext) -> Result<O, Error>;
60
61    /// Parses partial text during streaming; `None` when nothing usable is
62    /// available yet.
63    fn parse_partial(&self, text: &str) -> Option<JsonValue>;
64
65    /// Converts a partial JSON value into the typed output when it already
66    /// satisfies the schema.
67    fn typed_partial(&self, _value: &JsonValue) -> Option<O> {
68        None
69    }
70
71    /// Array strategies: the complete, validated elements contained in the
72    /// partial text so far.
73    fn parse_elements(&self, _text: &str) -> Option<Vec<JsonValue>> {
74        None
75    }
76}
77
78/// The strategy used when no output is requested: no response format, no
79/// parsing.
80#[derive(Debug, Clone, Copy, Default)]
81pub struct NoOutput;
82
83impl OutputHandler<()> for NoOutput {
84    fn response_format(&self) -> Option<ResponseFormat> {
85        None
86    }
87
88    fn wants_output(&self) -> bool {
89        false
90    }
91
92    fn parse_complete(&self, _text: &str, _ctx: &OutputContext) -> Result<(), Error> {
93        Ok(())
94    }
95
96    fn parse_partial(&self, _text: &str) -> Option<JsonValue> {
97        None
98    }
99}
100
101/// A structured output specification producing values of type `T`.
102pub struct Output<T> {
103    handler: Arc<dyn OutputHandler<T>>,
104}
105
106impl<T> Clone for Output<T> {
107    fn clone(&self) -> Self {
108        Self {
109            handler: Arc::clone(&self.handler),
110        }
111    }
112}
113
114impl<T> fmt::Debug for Output<T> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.write_str("Output(..)")
117    }
118}
119
120impl<T> Output<T> {
121    /// Wraps a custom strategy.
122    pub fn custom(handler: impl OutputHandler<T>) -> Self {
123        Self {
124            handler: Arc::new(handler),
125        }
126    }
127
128    /// The underlying handler.
129    #[must_use]
130    pub fn handler(&self) -> Arc<dyn OutputHandler<T>> {
131        Arc::clone(&self.handler)
132    }
133}
134
135impl Output<String> {
136    /// Plain text (the default when no output is configured).
137    #[must_use]
138    pub fn text() -> Self {
139        Self::custom(TextOutput)
140    }
141
142    /// One of `options`, enforced through a JSON schema enum.
143    #[must_use]
144    pub fn choice(options: impl IntoIterator<Item = impl Into<String>>) -> Self {
145        Self::custom(ChoiceOutput::new(options))
146    }
147}
148
149impl<T: DeserializeOwned + JsonSchema + Send + Sync + 'static> Output<T> {
150    /// A JSON object described by `T`'s derived schema.
151    #[must_use]
152    pub fn object() -> Self {
153        Self::custom(ObjectOutput::new(Schema::<T>::derived()))
154    }
155}
156
157impl<T: DeserializeOwned + Send + Sync + 'static> Output<T> {
158    /// A JSON object validated by `schema`.
159    #[must_use]
160    pub fn object_with(schema: Schema<T>) -> Self {
161        Self::custom(ObjectOutput::new(schema))
162    }
163}
164
165impl<T: DeserializeOwned + JsonSchema + Send + Sync + 'static> Output<Vec<T>> {
166    /// An array of elements described by `T`'s derived schema.
167    #[must_use]
168    pub fn array() -> Self {
169        Self::custom(ArrayOutput::new(Schema::<T>::derived()))
170    }
171}
172
173impl<T: DeserializeOwned + Send + Sync + 'static> Output<Vec<T>> {
174    /// An array of elements validated by `element`.
175    #[must_use]
176    pub fn array_with(element: Schema<T>) -> Self {
177        Self::custom(ArrayOutput::new(element))
178    }
179}
180
181impl Output<JsonValue> {
182    /// Unconstrained JSON.
183    #[must_use]
184    pub fn json() -> Self {
185        Self::custom(JsonOutput::new(None))
186    }
187
188    /// JSON validated by a raw JSON schema.
189    #[must_use]
190    pub fn json_with_schema(schema: JsonValue) -> Self {
191        Self::custom(JsonOutput::new(Some(schema)))
192    }
193}
194
195/// Marker for outputs that stream element by element (`Vec<T>`).
196pub trait ArrayElements {
197    /// The element type.
198    type Element;
199}
200
201impl<T> ArrayElements for Vec<T> {
202    type Element = T;
203}
204
205/// A partial structured output published while streaming.
206#[derive(Debug, Clone, PartialEq)]
207pub struct PartialOutput<T> {
208    /// The repaired partial JSON value.
209    pub value: JsonValue,
210    /// The typed value, when the partial JSON already satisfies the schema.
211    pub typed: Option<T>,
212}