Skip to main content

aither_core/llm/
mod.rs

1//! # Language Models and Conversation Management
2//!
3//! This module provides everything you need to work with language models in a provider-agnostic way.
4//! Build chat applications, generate structured output, and integrate tools without being tied to any specific AI service.
5//!
6//! ## Core Components
7//!
8//! - **[`LanguageModel`]** - The main trait for text generation and conversation
9//! - **[`LLMRequest`]** - Encapsulates messages, tools, and parameters for model calls
10//! - **[`Event`]** - Stream events from the model (text, reasoning, tool calls)
11//! - **[`Message`]** - Represents individual messages in a conversation
12//! - **[`Tool`]** - Function calling interface for extending model capabilities
13//!
14//! ## Design Philosophy
15//!
16//! The core crate provides a **low-level API** that emits events without executing tools.
17//! Tool execution is the responsibility of higher-level abstractions like `aither-agent`.
18//!
19//! This design allows:
20//! - Full control over tool execution flow
21//! - Hooks for intercepting and modifying tool calls
22//! - Proper context management between turns
23//! - Clean separation between LLM communication and agent logic
24//!
25//! ## Quick Start
26//!
27//! ### Basic Conversation
28//!
29//! ```rust,ignore
30//! use aither::llm::{LanguageModel, Event, oneshot};
31//! use futures_lite::StreamExt;
32//!
33//! async fn chat_with_model(model: impl LanguageModel) -> Result<String, Box<dyn std::error::Error>> {
34//!     let request = oneshot("You are a helpful assistant", "What's the capital of Japan?");
35//!     let mut stream = model.respond(request);
36//!     let mut full_text = String::new();
37//!
38//!     while let Some(event) = stream.next().await {
39//!         match event? {
40//!             Event::Text(chunk) => full_text.push_str(&chunk),
41//!             Event::Reasoning(thought) => eprintln!("[thinking] {}", thought),
42//!             Event::ToolCall(call) => {
43//!                 // Handle tool call (typically done by agent crate)
44//!                 println!("Tool requested: {}", call.name);
45//!             }
46//!             _ => {}
47//!         }
48//!     }
49//!
50//!     Ok(full_text)
51//! }
52//! ```
53//!
54//! ### With Tools (Agent-Controlled)
55//!
56//! ```rust,ignore
57//! use aither::llm::{LanguageModel, Event, LLMRequest, Message};
58//!
59//! // The core crate does NOT execute tools - it emits ToolCall events.
60//! // Tool execution should be handled by the agent crate.
61//! let request = LLMRequest::new([Message::user("What's the weather?")])
62//!     .with_tool_definitions(vec![weather_tool_definition()]);
63//!
64//! let mut stream = model.respond(request);
65//! while let Some(event) = stream.next().await {
66//!     match event? {
67//!         Event::ToolCall(call) => {
68//!             // Execute tool and continue conversation
69//!             let result = my_tool_executor.execute(&call).await;
70//!             // Add result to messages and send another request...
71//!         }
72//!         _ => {}
73//!     }
74//! }
75//! ```
76
77/// Assistant module for managing assistant-related functionality.
78pub mod assistant;
79/// Event types for streaming responses.
80pub mod event;
81/// Message types and conversation handling.
82pub mod message;
83/// Model profiles and capabilities.
84pub mod model;
85/// Provider module for managing language model providers and their configurations.
86pub mod provider;
87/// Provider-opaque reasoning state carried across turns.
88pub mod reasoning;
89/// Deep research workflows and agent capabilities.
90pub mod researcher;
91/// Tool system for function calling.
92pub mod tool;
93
94use crate::llm::{model::Parameters, tool::Tools};
95use alloc::{
96    boxed::Box,
97    string::{String, ToString},
98    sync::Arc,
99    vec,
100    vec::Vec,
101};
102use core::{any::TypeId, future::Future};
103pub use event::{Event, ToolCall, Usage};
104use futures_core::Stream;
105use futures_lite::{StreamExt, pin};
106pub use message::{Attachment, Message, Role};
107pub use provider::LanguageModelProvider;
108pub use reasoning::ReasoningState;
109pub use researcher::{
110    ResearchCitation, ResearchEvent, ResearchFinding, ResearchOptions, ResearchReport,
111    ResearchRequest, ResearchSource, ResearchStage, Researcher, ResearcherProfile,
112};
113use schemars::{JsonSchema, schema_for};
114use serde::de::DeserializeOwned;
115pub use tool::{
116    CommandOutcome, CommandResult, CommandStream, IntoToolResult, StoredFile, Tool, ToolContext,
117    ToolResult,
118};
119
120use crate::llm::model::Profile;
121
122/// Why a structured-output call failed.
123///
124/// [`LanguageModel::respond`] reports provider failures through the model's own
125/// [`LanguageModel::Error`]. Structured output adds a second, separate way to
126/// fail — the model answered, but not with the requested shape — so this keeps
127/// the two distinguishable instead of flattening both into one opaque error.
128/// Callers need that distinction: a rate limit is worth retrying, a schema
129/// mismatch is worth re-prompting, and an auth failure is worth neither.
130#[derive(Debug)]
131pub enum GenerateError<E> {
132    /// The provider itself failed: transport, rate limit, auth, and so on.
133    Provider(E),
134
135    /// The provider answered, but the response did not match the schema.
136    ///
137    /// Carries the text that could not be parsed so callers can log or retry.
138    Parse {
139        /// The underlying deserialization failure.
140        source: serde_json::Error,
141        /// The response text that failed to parse, truncated for logging.
142        response: String,
143    },
144}
145
146impl<E: core::fmt::Display> core::fmt::Display for GenerateError<E> {
147    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148        match self {
149            Self::Provider(err) => write!(f, "language model request failed: {err}"),
150            Self::Parse { source, response } => {
151                write!(
152                    f,
153                    "structured output did not match the requested schema: {source}; response: {response}"
154                )
155            }
156        }
157    }
158}
159
160impl<E: core::error::Error + 'static> core::error::Error for GenerateError<E> {
161    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
162        match self {
163            Self::Provider(err) => Some(err),
164            Self::Parse { source, .. } => Some(source),
165        }
166    }
167}
168
169/// Builder-style request passed into [`LanguageModel::respond`].
170///
171/// Wraps the full conversation, model parameters, and tool definitions a provider
172/// needs in order to execute a call.
173#[derive(Debug, Clone)]
174pub struct LLMRequest {
175    messages: Vec<Message>,
176    parameters: Parameters,
177    tool_definitions: Vec<tool::ToolDefinition>,
178}
179
180impl LLMRequest {
181    /// Creates a request from the provided messages using default parameters.
182    pub fn new(messages: impl Into<Vec<Message>>) -> Self {
183        Self {
184            messages: messages.into(),
185            parameters: Parameters::default(),
186            tool_definitions: Vec::new(),
187        }
188    }
189
190    /// Adds tool definitions to the request.
191    ///
192    /// These define what tools the model can request. The model will emit
193    /// [`Event::ToolCall`] events when it wants to use a tool.
194    #[must_use]
195    pub fn with_tool_definitions(mut self, definitions: Vec<tool::ToolDefinition>) -> Self {
196        self.tool_definitions = definitions;
197        self
198    }
199
200    /// Adds a single tool definition.
201    #[must_use]
202    pub fn with_tool<T: Tool>(mut self, tool: &T) -> Self {
203        self.tool_definitions.push(tool::ToolDefinition::new(tool));
204        self
205    }
206
207    /// Overrides the sampling parameters used for this call.
208    #[must_use]
209    pub fn with_parameters(mut self, parameters: Parameters) -> Self {
210        self.parameters = parameters;
211        self
212    }
213
214    /// Returns the current conversation messages.
215    #[must_use]
216    pub fn messages(&self) -> &[Message] {
217        &self.messages
218    }
219
220    /// Returns a mutable reference to messages for modification.
221    pub const fn messages_mut(&mut self) -> &mut Vec<Message> {
222        &mut self.messages
223    }
224
225    /// Returns the current parameter snapshot.
226    #[must_use]
227    pub const fn parameters(&self) -> &Parameters {
228        &self.parameters
229    }
230
231    /// Returns the tool definitions.
232    #[must_use]
233    pub fn tool_definitions(&self) -> &[tool::ToolDefinition] {
234        &self.tool_definitions
235    }
236
237    /// Breaks the request into owned components.
238    #[must_use]
239    pub fn into_parts(self) -> (Vec<Message>, Parameters, Vec<tool::ToolDefinition>) {
240        (self.messages, self.parameters, self.tool_definitions)
241    }
242}
243
244/// Legacy request builder that supports mutable tool registry.
245///
246/// This is provided for backwards compatibility with providers that
247/// handle tool execution internally (built-in tools).
248#[derive(Debug)]
249pub struct LLMRequestWithTools<'tools> {
250    inner: LLMRequest,
251    tools: &'tools mut Tools,
252}
253
254impl LLMRequest {
255    /// Attaches a mutable tool registry to the request.
256    ///
257    /// This is for providers that execute tools internally (e.g., built-in tools).
258    /// For agent-controlled tool execution, use `with_tool_definitions` instead.
259    pub fn with_tools(self, tools: &mut Tools) -> LLMRequestWithTools<'_> {
260        let definitions = tools.definitions();
261        LLMRequestWithTools {
262            inner: self.with_tool_definitions(definitions),
263            tools,
264        }
265    }
266}
267
268impl<'tools> LLMRequestWithTools<'tools> {
269    /// Returns the inner request.
270    #[must_use]
271    pub const fn request(&self) -> &LLMRequest {
272        &self.inner
273    }
274
275    /// Returns the tool registry.
276    #[must_use]
277    pub const fn tools(&mut self) -> &mut Tools {
278        self.tools
279    }
280
281    /// Breaks into components.
282    #[must_use]
283    pub fn into_parts(self) -> (LLMRequest, &'tools mut Tools) {
284        (self.inner, self.tools)
285    }
286
287    /// Invokes a registered tool by name, running it with `cx` as its
288    /// [`ToolContext`].
289    ///
290    /// # Errors
291    /// Returns an error if tool is not found or the tool call fails.
292    pub async fn call_tool(
293        &mut self,
294        name: &str,
295        args_json: &str,
296        cx: ToolContext,
297    ) -> crate::Result<ToolResult> {
298        self.tools.call(name, args_json, cx).await
299    }
300}
301
302/// Language models for text generation and conversation.
303///
304/// The `respond` method returns a stream of [`Event`]s. Tool calls are emitted
305/// as events and are NOT automatically executed - this allows higher-level
306/// abstractions (like agents) to control tool execution.
307///
308/// See the [module documentation](crate::llm) for examples and usage patterns.
309pub trait LanguageModel: Sized + Send + Sync {
310    /// The error type returned by this language model.
311    type Error: core::error::Error + Send + Sync + 'static;
312
313    /// Generates a streaming response to a conversation.
314    ///
315    /// Returns a stream of [`Event`]s including:
316    /// - `Event::Text` - Visible text chunks
317    /// - `Event::Reasoning` - Internal reasoning (for reasoning models)
318    /// - `Event::ToolCall` - Requests to execute tools (NOT auto-executed)
319    /// - `Event::BuiltInToolResult` - Results from provider's built-in tools
320    fn respond(&self, request: LLMRequest)
321    -> impl Stream<Item = Result<Event, Self::Error>> + Send;
322
323    /// Generates a streaming response with a mutable tool registry.
324    ///
325    /// This is for providers that support built-in tool execution.
326    /// The default implementation ignores the tools and delegates to `respond`.
327    fn respond_with_tools(
328        &self,
329        request: LLMRequestWithTools<'_>,
330    ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
331        let (inner, _tools) = request.into_parts();
332        self.respond(inner)
333    }
334
335    /// Generates structured output conforming to JSON schema.
336    ///
337    /// # Note for Implementors
338    /// By default, we use a system prompt to instruct the model to generate structured output based on the provided JSON schema.
339    /// However, that is not efficient enough, if supported, provider should override this method to provide native structured generation support.
340    /// Native structured generation can apply decode rules in token-level, ensuring the output is always valid JSON, and reducing parsing errors.
341    fn generate<T: JsonSchema + DeserializeOwned + 'static>(
342        &self,
343        request: LLMRequest,
344    ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
345        async { structured_generate(self, request).await }
346    }
347
348    /// Completes given text prefix.
349    fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
350        self.respond(oneshot("Please complete the following text:", prefix))
351    }
352
353    /// Summarizes text.
354    ///
355    /// # Note for Implementors
356    /// By default, we provide a generic summarization prompt. However, some model provider, like Apple Intelligence, may have native summarization support.
357    /// It would load a summarization-specific lora at runtime, providing better quality.
358    fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
359        summarize(self, text)
360    }
361
362    /// Categorizes text.
363    ///
364    /// # Note for Implementors
365    /// By default, we use a system prompt to instruct the model to categorize text based on the provided JSON schema.
366    /// However, that is not efficient enough, if supported, provider should override this method to provide native categorization support.
367    fn categorize<T: JsonSchema + DeserializeOwned + 'static>(
368        &self,
369        text: &str,
370    ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
371        async { categorize_text(self, text).await }
372    }
373
374    /// Returns model profile and capabilities.
375    ///
376    /// See [`Profile`] for details on model metadata.
377    ///
378    /// # Errors
379    ///
380    /// Returns an error when the profile cannot be determined — for example
381    /// when neither the provider nor the model registry knows the model's
382    /// context window. Implementations must not substitute a guessed value.
383    fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send;
384}
385
386macro_rules! impl_language_model {
387    ($($name:ident),*) => {
388        $(
389            impl<T: LanguageModel> LanguageModel for $name<T> {
390                type Error = T::Error;
391
392                fn respond(
393                    &self,
394                    request: LLMRequest,
395                ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
396                    T::respond(self, request)
397                }
398
399                fn respond_with_tools(
400                    &self,
401                    request: LLMRequestWithTools<'_>,
402                ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
403                    T::respond_with_tools(self, request)
404                }
405
406                fn generate<U: JsonSchema + DeserializeOwned + 'static>(
407                    &self,
408                    request: LLMRequest,
409                ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
410                    T::generate(self, request)
411                }
412
413                fn complete(
414                    &self,
415                    prefix: &str,
416                ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
417                    T::complete(self, prefix)
418                }
419
420                fn summarize(
421                    &self,
422                    text: &str,
423                ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
424                    T::summarize(self, text)
425                }
426
427                fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
428                    &self,
429                    text: &str,
430                ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
431                    T::categorize(self, text)
432                }
433
434                fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
435                    T::profile(self)
436                }
437            }
438        )*
439    };
440}
441
442impl<T: LanguageModel> LanguageModel for &T {
443    type Error = T::Error;
444
445    fn respond(
446        &self,
447        request: LLMRequest,
448    ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
449        T::respond(self, request)
450    }
451
452    fn respond_with_tools(
453        &self,
454        request: LLMRequestWithTools<'_>,
455    ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
456        T::respond_with_tools(self, request)
457    }
458
459    fn generate<U: JsonSchema + DeserializeOwned + 'static>(
460        &self,
461        request: LLMRequest,
462    ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
463        T::generate(self, request)
464    }
465
466    fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
467        T::complete(self, prefix)
468    }
469
470    fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
471        T::summarize(self, text)
472    }
473
474    fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
475        &self,
476        text: &str,
477    ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
478        T::categorize(self, text)
479    }
480
481    fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
482        T::profile(self)
483    }
484}
485
486mod prompts;
487
488impl_language_model!(Arc, Box);
489
490/// Collects text from an event stream.
491///
492/// # Errors
493///
494/// Returns the first stream error encountered while collecting text chunks.
495pub async fn collect_text<S, E>(stream: S) -> Result<String, E>
496where
497    S: Stream<Item = Result<Event, E>>,
498{
499    pin!(stream);
500    let mut result = String::new();
501    while let Some(event) = stream.next().await {
502        if let Event::Text(text) = event? {
503            result.push_str(&text);
504        }
505    }
506    Ok(result)
507}
508
509async fn structured_generate<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
510    model: &M,
511    mut request: LLMRequest,
512) -> Result<T, GenerateError<M::Error>> {
513    let schema = schema_for!(T);
514
515    // If it is a string, we are not required to set up structured generation and JSON schema.
516    let json = if schema.as_value().is_string() {
517        let stream = model.respond(request);
518        let response = collect_text(stream)
519            .await
520            .map_err(GenerateError::Provider)?;
521        // Let's encode it as JSON string. Serializing a String cannot fail.
522        serde_json::to_string(&response).map_err(|source| GenerateError::Parse {
523            source,
524            response: response.clone(),
525        })?
526    } else {
527        // Serializing a `Schema` is infallible in practice, but keep the error
528        // typed rather than unwrapping.
529        let schema =
530            serde_json::to_string_pretty(&schema).map_err(|source| GenerateError::Parse {
531                source,
532                response: String::new(),
533            })?;
534        let prompt = prompts::generate(&schema);
535        request.messages.push(Message::system(prompt));
536        request.parameters.structured_outputs = true;
537
538        let stream = model.respond(request);
539        collect_text(stream)
540            .await
541            .map_err(GenerateError::Provider)?
542    };
543
544    parse_json_with_recovery(&json).map_err(|source| GenerateError::Parse {
545        source,
546        response: truncate_for_error(&json),
547    })
548}
549
550/// Keeps a failed response short enough to log without dumping a whole context.
551fn truncate_for_error(response: &str) -> String {
552    const LIMIT: usize = 500;
553    response.chars().take(LIMIT).collect()
554}
555
556/// Convenience helper that creates a single system + user [`LLMRequest`].
557pub fn oneshot(system: impl Into<String>, user: impl Into<String>) -> LLMRequest {
558    let messages = vec![Message::system(system.into()), Message::user(user.into())];
559    LLMRequest::new(messages)
560}
561
562fn summarize<M: LanguageModel>(
563    model: &M,
564    text: &str,
565) -> impl Stream<Item = Result<Event, M::Error>> + Send {
566    let messages = oneshot("Summarize text:", text);
567    model.respond(messages)
568}
569
570async fn categorize_text<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
571    model: &M,
572    text: &str,
573) -> Result<T, GenerateError<M::Error>> {
574    let request = oneshot("Categorize text by provided schema", text);
575    model.generate(request).await
576}
577
578fn parse_json_with_recovery<T: DeserializeOwned + 'static>(
579    json: &str,
580) -> Result<T, serde_json::Error> {
581    use serde::de::Error as _;
582
583    let trimmed = json.trim();
584    let mut last_error: Option<serde_json::Error> = None;
585    let mut last_candidate: Option<String> = None;
586
587    for candidate in build_json_candidates(trimmed) {
588        match serde_json::from_str::<T>(&candidate) {
589            Ok(value) => return Ok(value),
590            Err(err) => {
591                last_error = Some(err);
592                last_candidate = Some(candidate);
593            }
594        }
595    }
596
597    // A model asked for a plain string will often answer with an object or a
598    // bare word instead. Re-encode whatever it did produce as a JSON string.
599    if is_string_type::<T>()
600        && let Some(candidate) = last_candidate
601        && let Ok(value) = serde_json::from_str::<serde_json::Value>(&candidate)
602    {
603        let text = match value {
604            serde_json::Value::String(s) => s,
605            other => other.to_string(),
606        };
607        let encoded = serde_json::to_string(&text)?;
608        if let Ok(value) = serde_json::from_str::<T>(&encoded) {
609            return Ok(value);
610        }
611    }
612
613    Err(last_error.unwrap_or_else(|| {
614        serde_json::Error::custom("structured output was empty or missing a JSON block")
615    }))
616}
617
618fn strip_code_fences(raw: &str) -> Option<String> {
619    let trimmed = raw.trim();
620    let fence_start = trimmed.find("```")?;
621    let after_fence = &trimmed[fence_start + 3..];
622    let mut lines = after_fence.lines();
623    let _maybe_lang = lines.next();
624    let body = lines.collect::<Vec<_>>().join("\n");
625    let content = body.rfind("```").map_or(body.as_str(), |end| &body[..end]);
626
627    let cleaned = content.trim();
628    if cleaned.is_empty() {
629        None
630    } else {
631        Some(cleaned.to_string())
632    }
633}
634
635fn extract_json_block(raw: &str) -> Option<String> {
636    if let (Some(start), Some(end)) = (raw.find('{'), raw.rfind('}'))
637        && end >= start
638    {
639        let candidate = &raw[start..=end];
640        if !candidate.trim().is_empty() {
641            return Some(candidate.trim().to_string());
642        }
643    }
644    if let (Some(start), Some(end)) = (raw.find('['), raw.rfind(']'))
645        && end >= start
646    {
647        let candidate = &raw[start..=end];
648        if !candidate.trim().is_empty() {
649            return Some(candidate.trim().to_string());
650        }
651    }
652    None
653}
654
655fn build_json_candidates(raw: &str) -> Vec<String> {
656    let mut candidates = Vec::new();
657
658    if !raw.is_empty() {
659        candidates.push(raw.to_string());
660    }
661
662    if let Some(fenced) = strip_code_fences(raw) {
663        candidates.push(fenced);
664    }
665
666    if let Some(block) = extract_json_block(raw) {
667        candidates.push(block);
668    }
669
670    if let Some(dequoted) = dequote_json_string(raw) {
671        candidates.push(dequoted);
672    }
673
674    if let Some(stripped) = strip_leading_label(raw, "json") {
675        candidates.push(stripped);
676    }
677
678    let mut deduped = Vec::new();
679    for candidate in candidates {
680        if deduped.iter().all(|seen| seen != &candidate) {
681            deduped.push(candidate);
682        }
683    }
684    deduped
685}
686
687fn dequote_json_string(raw: &str) -> Option<String> {
688    let trimmed = raw.trim();
689    if !(trimmed.starts_with('"') && trimmed.ends_with('"')) {
690        return None;
691    }
692    let inner: String = serde_json::from_str(trimmed).ok()?;
693    if inner.trim().is_empty() {
694        None
695    } else {
696        Some(inner)
697    }
698}
699
700fn strip_leading_label(raw: &str, label: &str) -> Option<String> {
701    let trimmed = raw.trim_start();
702    if !trimmed.to_ascii_lowercase().starts_with(label) {
703        return None;
704    }
705    let stripped = trimmed[label.len()..]
706        .trim_start_matches(|c: char| c.is_whitespace() || c == ':' || c == '-')
707        .trim();
708    if stripped.is_empty() {
709        None
710    } else {
711        Some(stripped.to_string())
712    }
713}
714
715fn is_string_type<T: 'static>() -> bool {
716    TypeId::of::<T>() == TypeId::of::<String>()
717}
718
719#[cfg(test)]
720mod tests {
721    use super::parse_json_with_recovery;
722    use alloc::string::String;
723    use serde::Deserialize;
724
725    #[derive(Debug, Deserialize, PartialEq, Eq)]
726    struct Foo {
727        a: u8,
728    }
729
730    #[test]
731    fn parses_plain_json() {
732        let foo: Foo = parse_json_with_recovery(r#"{"a":1}"#).unwrap();
733        assert_eq!(foo, Foo { a: 1 });
734    }
735
736    #[test]
737    fn parses_code_fence_json() {
738        let foo: Foo = parse_json_with_recovery("```json\n{\"a\":2}\n```").unwrap();
739        assert_eq!(foo, Foo { a: 2 });
740    }
741
742    #[test]
743    fn parses_embedded_block() {
744        let foo: Foo = parse_json_with_recovery("noise {\"a\":3} trailing").unwrap();
745        assert_eq!(foo, Foo { a: 3 });
746    }
747
748    #[test]
749    fn parses_quoted_json_string() {
750        let foo: Foo = parse_json_with_recovery(r#""{\"a\":4}""#).unwrap();
751        assert_eq!(foo, Foo { a: 4 });
752    }
753
754    #[test]
755    fn parses_labeled_json() {
756        let foo: Foo = parse_json_with_recovery("json {\"a\":5}").unwrap();
757        assert_eq!(foo, Foo { a: 5 });
758    }
759
760    #[test]
761    fn coerces_object_to_string() {
762        let value: String =
763            parse_json_with_recovery(r#"{"title":"summary","type":"content"}"#).unwrap();
764        assert!(
765            value.contains("\"title\":\"summary\"") && value.contains("\"type\":\"content\""),
766            "unexpected value: {value}"
767        );
768    }
769}