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