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