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