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