Skip to main content

llm_stack/
provider.rs

1//! Provider trait and request types.
2//!
3//! This module defines two core abstractions:
4//!
5//! - **[`Provider`]** — the trait every backend implements. It uses Rust
6//!   2024's native async-fn-in-traits (AFIT), so implementations are
7//!   straightforward `async fn`s with no macro overhead.
8//!
9//! - **[`DynProvider`]** — an object-safe mirror of `Provider` that uses
10//!   boxed futures. A blanket `impl<T: Provider> DynProvider for T`
11//!   bridges the two, so any concrete provider can be stored as
12//!   `Box<dyn DynProvider>` or `Arc<dyn DynProvider>` with zero
13//!   boilerplate.
14//!
15//! # When to use which
16//!
17//! | Situation | Use |
18//! |-----------|-----|
19//! | Generic code that knows the concrete type | `Provider` |
20//! | Need to store providers in a collection or behind `dyn` | `DynProvider` |
21//! | Implementing a new backend | `impl Provider for MyBackend` |
22//!
23//! # Request parameters
24//!
25//! All request configuration lives in [`ChatParams`]. It serializes
26//! cleanly to JSON (for logging / replay) with the exception of
27//! [`timeout`](ChatParams::timeout) and
28//! [`extra_headers`](ChatParams::extra_headers), which are transport
29//! concerns and are `#[serde(skip)]`'d.
30
31use std::borrow::Cow;
32use std::collections::{HashMap, HashSet};
33use std::future::Future;
34use std::pin::Pin;
35use std::time::Duration;
36
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39
40use crate::chat::{ChatMessage, ChatResponse};
41use crate::error::LlmError;
42use crate::stream::ChatStream;
43
44/// The core trait every LLM provider implements.
45///
46/// `Provider` uses native async-fn-in-traits (Rust 2024 edition).
47/// Implementations are plain `async fn`s — no `#[async_trait]` needed.
48///
49/// Cross-cutting concerns like retries, rate-limiting, and logging are
50/// handled by the interceptor system, keeping individual backends focused
51/// on HTTP mapping.
52///
53/// # Object safety
54///
55/// `Provider` is **not** object-safe because AFIT returns `impl Future`.
56/// When you need dynamic dispatch (e.g. `Box<dyn _>` or `Arc<dyn _>`),
57/// use [`DynProvider`] instead — every `Provider` automatically
58/// implements `DynProvider` via a blanket impl.
59pub trait Provider: Send + Sync {
60    /// Sends a chat completion request and returns the full response.
61    fn generate(
62        &self,
63        params: &ChatParams,
64    ) -> impl Future<Output = Result<ChatResponse, LlmError>> + Send;
65
66    /// Sends a chat completion request and returns a stream of events.
67    ///
68    /// The returned [`ChatStream`] yields [`StreamEvent`](crate::StreamEvent)s
69    /// as they arrive from the provider.
70    fn stream(
71        &self,
72        params: &ChatParams,
73    ) -> impl Future<Output = Result<ChatStream, LlmError>> + Send;
74
75    /// Returns static metadata describing this provider instance.
76    fn metadata(&self) -> ProviderMetadata;
77}
78
79/// Object-safe counterpart of [`Provider`] for dynamic dispatch.
80///
81/// You rarely implement this directly — the blanket
82/// `impl<T: Provider> DynProvider for T` does it for you. Use this
83/// when you need to erase the concrete provider type:
84///
85/// ```rust,no_run
86/// use llm_stack::{DynProvider, ChatParams};
87///
88/// async fn ask(provider: &dyn DynProvider, question: &str) -> String {
89///     let params = ChatParams {
90///         messages: vec![llm_stack::ChatMessage::user(question)],
91///         ..Default::default()
92///     };
93///     let resp = provider.generate_boxed(&params).await.unwrap();
94///     format!("{resp:?}")
95/// }
96/// ```
97pub trait DynProvider: Send + Sync {
98    /// Boxed-future version of [`Provider::generate`].
99    fn generate_boxed<'a>(
100        &'a self,
101        params: &'a ChatParams,
102    ) -> Pin<Box<dyn Future<Output = Result<ChatResponse, LlmError>> + Send + 'a>>;
103
104    /// Boxed-future version of [`Provider::stream`].
105    fn stream_boxed<'a>(
106        &'a self,
107        params: &'a ChatParams,
108    ) -> Pin<Box<dyn Future<Output = Result<ChatStream, LlmError>> + Send + 'a>>;
109
110    /// Returns static metadata describing this provider instance.
111    fn metadata(&self) -> ProviderMetadata;
112}
113
114impl<T: Provider> DynProvider for T {
115    fn generate_boxed<'a>(
116        &'a self,
117        params: &'a ChatParams,
118    ) -> Pin<Box<dyn Future<Output = Result<ChatResponse, LlmError>> + Send + 'a>> {
119        Box::pin(self.generate(params))
120    }
121
122    fn stream_boxed<'a>(
123        &'a self,
124        params: &'a ChatParams,
125    ) -> Pin<Box<dyn Future<Output = Result<ChatStream, LlmError>> + Send + 'a>> {
126        Box::pin(self.stream(params))
127    }
128
129    fn metadata(&self) -> ProviderMetadata {
130        Provider::metadata(self)
131    }
132}
133
134/// Describes a provider instance: its name, model, and capabilities.
135///
136/// The `name` field uses [`Cow<'static, str>`] so that built-in
137/// providers can use `"anthropic"` (zero-alloc) while dynamic or
138/// user-created providers can use owned strings.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ProviderMetadata {
141    /// Human-readable provider name (e.g. `"anthropic"`, `"openai"`).
142    pub name: Cow<'static, str>,
143    /// The model identifier (e.g. `"claude-sonnet-4-20250514"`).
144    pub model: String,
145    /// Maximum context window size in tokens.
146    pub context_window: u64,
147    /// Feature flags indicating what this provider supports.
148    pub capabilities: HashSet<Capability>,
149}
150
151/// A feature that a provider may or may not support.
152///
153/// Callers can inspect [`ProviderMetadata::capabilities`] to decide
154/// whether to include tool definitions, request structured output, etc.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[non_exhaustive]
157pub enum Capability {
158    /// Function/tool calling.
159    Tools,
160    /// JSON Schema–constrained output.
161    StructuredOutput,
162    /// Extended chain-of-thought reasoning.
163    Reasoning,
164    /// Image (and potentially video) understanding.
165    Vision,
166    /// Prompt caching for reduced latency and cost.
167    Caching,
168}
169
170/// Parameters for a chat completion request.
171///
172/// Most fields are optional — at minimum you need [`messages`](Self::messages).
173/// Use struct-update syntax for concise construction:
174///
175/// ```rust
176/// use llm_stack::{ChatParams, ChatMessage};
177///
178/// let params = ChatParams {
179///     messages: vec![ChatMessage::user("Hello")],
180///     max_tokens: Some(256),
181///     temperature: Some(0.7),
182///     ..Default::default()
183/// };
184/// ```
185///
186/// # Serialization
187///
188/// `ChatParams` implements `Serialize` / `Deserialize` for logging and
189/// request replay. The [`timeout`](Self::timeout) and
190/// [`extra_headers`](Self::extra_headers) fields are skipped during
191/// serialization because they are transport-layer concerns, not part of
192/// the logical request.
193///
194/// # Equality
195///
196/// `PartialEq` is structural. The `extra_headers` field uses
197/// `http::HeaderMap`, whose equality comparison is order-sensitive for
198/// multi-valued headers. In practice this only matters in tests via
199/// [`MockProvider::recorded_calls`](crate::mock::MockProvider::recorded_calls).
200#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
201pub struct ChatParams {
202    /// The conversation history.
203    pub messages: Vec<ChatMessage>,
204    /// Tool definitions the model may invoke.
205    pub tools: Option<Vec<ToolDefinition>>,
206    /// Controls whether and how the model uses tools.
207    pub tool_choice: Option<ToolChoice>,
208    /// Sampling temperature (0.0 = deterministic, higher = more random).
209    pub temperature: Option<f32>,
210    /// Upper bound on generated tokens.
211    pub max_tokens: Option<u32>,
212    /// System prompt (used by providers that accept it separately from
213    /// the message list).
214    pub system: Option<String>,
215    /// Token budget for chain-of-thought reasoning, if the provider
216    /// supports [`Capability::Reasoning`].
217    pub reasoning_budget: Option<u32>,
218    /// JSON Schema that the model's output must conform to.
219    pub structured_output: Option<JsonSchema>,
220    /// Per-request timeout. Skipped during serialization.
221    #[serde(skip)]
222    pub timeout: Option<Duration>,
223    /// Extra HTTP headers to send with this request. Skipped during
224    /// serialization.
225    #[serde(skip)]
226    pub extra_headers: Option<http::HeaderMap>,
227    /// Arbitrary key-value pairs forwarded to the provider. Useful for
228    /// provider-specific features that don't have a dedicated field.
229    pub metadata: HashMap<String, Value>,
230}
231
232/// Controls whether the model should use tools and, if so, which ones.
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[non_exhaustive]
235pub enum ToolChoice {
236    /// The model decides whether to call a tool.
237    Auto,
238    /// The model must not call any tools.
239    None,
240    /// The model must call at least one tool.
241    Required,
242    /// The model must call this specific tool.
243    Specific(String),
244}
245
246/// Retry behavior predicate type.
247///
248/// Receives the error message and returns `true` if the error is retryable.
249pub type RetryPredicate = std::sync::Arc<dyn Fn(&str) -> bool + Send + Sync>;
250
251/// Configuration for automatic retries when a tool execution fails.
252///
253/// When a tool handler returns an error and retry configuration is present,
254/// the registry will automatically retry with exponential backoff.
255///
256/// # Example
257///
258/// ```rust
259/// use llm_stack::provider::ToolRetryConfig;
260/// use std::time::Duration;
261///
262/// let config = ToolRetryConfig {
263///     max_retries: 3,
264///     initial_backoff: Duration::from_millis(100),
265///     max_backoff: Duration::from_secs(5),
266///     backoff_multiplier: 2.0,
267///     jitter: 0.5,
268///     retry_if: None, // Retry all errors
269/// };
270/// ```
271#[derive(Clone)]
272pub struct ToolRetryConfig {
273    /// Maximum retry attempts (not counting initial try). Default: 3.
274    pub max_retries: u32,
275    /// Initial backoff duration before first retry. Default: 100ms.
276    pub initial_backoff: Duration,
277    /// Maximum backoff duration cap. Default: 5 seconds.
278    pub max_backoff: Duration,
279    /// Backoff multiplier for exponential growth. Default: 2.0.
280    pub backoff_multiplier: f64,
281    /// Jitter factor (0.0 to 1.0) applied to backoff. Default: 0.5.
282    pub jitter: f64,
283    /// Optional predicate to determine if an error is retryable.
284    /// Receives the error message. If `None`, all errors are retried.
285    pub retry_if: Option<RetryPredicate>,
286}
287
288impl Default for ToolRetryConfig {
289    fn default() -> Self {
290        Self {
291            max_retries: 3,
292            initial_backoff: Duration::from_millis(100),
293            max_backoff: Duration::from_secs(5),
294            backoff_multiplier: 2.0,
295            jitter: 0.5,
296            retry_if: None,
297        }
298    }
299}
300
301impl std::fmt::Debug for ToolRetryConfig {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.debug_struct("ToolRetryConfig")
304            .field("max_retries", &self.max_retries)
305            .field("initial_backoff", &self.initial_backoff)
306            .field("max_backoff", &self.max_backoff)
307            .field("backoff_multiplier", &self.backoff_multiplier)
308            .field("jitter", &self.jitter)
309            .field("has_retry_if", &self.retry_if.is_some())
310            .finish()
311    }
312}
313
314impl PartialEq for ToolRetryConfig {
315    fn eq(&self, other: &Self) -> bool {
316        self.max_retries == other.max_retries
317            && self.initial_backoff == other.initial_backoff
318            && self.max_backoff == other.max_backoff
319            && self.backoff_multiplier == other.backoff_multiplier
320            && self.jitter == other.jitter
321            && self.retry_if.is_some() == other.retry_if.is_some()
322    }
323}
324
325/// A tool the model can invoke during generation.
326///
327/// Providers translate this into their native tool format.
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
329pub struct ToolDefinition {
330    /// The tool's name, used to match [`ToolCall::name`](crate::ToolCall::name).
331    pub name: String,
332    /// Human-readable description shown to the model so it knows when
333    /// to use this tool.
334    pub description: String,
335    /// JSON Schema describing the tool's expected input.
336    pub parameters: JsonSchema,
337    /// Optional retry configuration for this tool.
338    ///
339    /// When present, failed tool executions will be automatically retried
340    /// with exponential backoff. This is a **runtime-only** setting — it is
341    /// not serialized (skipped during serde) because it's not part of the
342    /// tool schema sent to the LLM.
343    #[serde(skip)]
344    pub retry: Option<ToolRetryConfig>,
345}
346
347/// A JSON Schema document used for structured output or tool parameters.
348///
349/// Wraps a [`serde_json::Value`] and provides validation via the
350/// [`jsonschema`] crate. The inner value is private — use
351/// [`as_value`](Self::as_value) for read access.
352///
353/// # Construction
354///
355/// ```rust
356/// use llm_stack::JsonSchema;
357///
358/// // From a raw JSON value
359/// let schema = JsonSchema::new(serde_json::json!({
360///     "type": "object",
361///     "properties": { "name": { "type": "string" } },
362///     "required": ["name"]
363/// }));
364///
365/// // From a Rust type that implements schemars::JsonSchema
366/// // let schema = JsonSchema::from_type::<MyStruct>()?;
367/// ```
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
369pub struct JsonSchema(Value);
370
371impl JsonSchema {
372    /// Creates a schema from a raw JSON value.
373    pub fn new(schema: Value) -> Self {
374        Self(schema)
375    }
376
377    /// Returns a reference to the underlying JSON value.
378    pub fn as_value(&self) -> &Value {
379        &self.0
380    }
381
382    /// Derives a JSON Schema from a Rust type that implements
383    /// [`schemars::JsonSchema`].
384    ///
385    /// Returns an error if the generated schema cannot be serialized to
386    /// `serde_json::Value` (should not happen in practice).
387    ///
388    /// Requires the `schema` feature (enabled by default).
389    #[cfg(feature = "schema")]
390    pub fn from_type<T: schemars::JsonSchema>() -> Result<Self, serde_json::Error> {
391        let schema = schemars::schema_for!(T);
392        let value = serde_json::to_value(schema)?;
393        Ok(Self(value))
394    }
395
396    /// Validates `value` against this schema.
397    ///
398    /// Requires the `schema` feature (enabled by default).
399    ///
400    /// Returns `Ok(())` if validation passes, or
401    /// [`LlmError::SchemaValidation`] with details on failure. Returns
402    /// [`LlmError::InvalidRequest`] if the schema itself is malformed.
403    #[cfg(feature = "schema")]
404    pub fn validate(&self, value: &Value) -> Result<(), LlmError> {
405        let validator = jsonschema::validator_for(&self.0)
406            .map_err(|e| LlmError::InvalidRequest(format!("invalid JSON schema: {e}")))?;
407        let errors: Vec<String> = validator
408            .iter_errors(value)
409            .map(|e| e.to_string())
410            .collect();
411        if errors.is_empty() {
412            Ok(())
413        } else {
414            Err(LlmError::SchemaValidation {
415                message: errors.join("; "),
416                schema: self.0.clone(),
417                actual: value.clone(),
418            })
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    // --- Capability tests ---
428
429    #[test]
430    fn test_capability_hash_set() {
431        let caps: HashSet<Capability> = HashSet::from([
432            Capability::Tools,
433            Capability::StructuredOutput,
434            Capability::Reasoning,
435            Capability::Vision,
436            Capability::Caching,
437        ]);
438        assert_eq!(caps.len(), 5);
439    }
440
441    #[test]
442    fn test_capability_copy() {
443        let c = Capability::Tools;
444        let c2 = c; // Copy
445        assert_eq!(c, c2);
446    }
447
448    #[test]
449    fn test_capability_serde_roundtrip() {
450        let cap = Capability::Tools;
451        let json = serde_json::to_string(&cap).unwrap();
452        let back: Capability = serde_json::from_str(&json).unwrap();
453        assert_eq!(cap, back);
454    }
455
456    // --- ProviderMetadata tests ---
457
458    #[test]
459    fn test_provider_metadata_clone_eq() {
460        let m = ProviderMetadata {
461            name: "mock".into(),
462            model: "test-model".into(),
463            context_window: 128_000,
464            capabilities: HashSet::from([Capability::Tools]),
465        };
466        assert_eq!(m, m.clone());
467    }
468
469    #[test]
470    fn test_provider_metadata_owned_name() {
471        let name = String::from("custom-provider");
472        let m = ProviderMetadata {
473            name: Cow::Owned(name),
474            model: "test".into(),
475            context_window: 4096,
476            capabilities: HashSet::new(),
477        };
478        assert_eq!(m.name, "custom-provider");
479    }
480
481    // --- ChatParams tests ---
482
483    #[test]
484    fn test_chat_params_defaults() {
485        let p = ChatParams::default();
486        assert!(p.messages.is_empty());
487        assert!(p.tools.is_none());
488        assert!(p.tool_choice.is_none());
489        assert!(p.temperature.is_none());
490        assert!(p.max_tokens.is_none());
491        assert!(p.system.is_none());
492        assert!(p.reasoning_budget.is_none());
493        assert!(p.structured_output.is_none());
494        assert!(p.timeout.is_none());
495        assert!(p.extra_headers.is_none());
496        assert!(p.metadata.is_empty());
497    }
498
499    #[test]
500    fn test_chat_params_full() {
501        let p = ChatParams {
502            messages: vec![ChatMessage::user("hi")],
503            tools: Some(vec![]),
504            tool_choice: Some(ToolChoice::Auto),
505            temperature: Some(0.7),
506            max_tokens: Some(1024),
507            system: Some("you are helpful".into()),
508            reasoning_budget: Some(2048),
509            structured_output: Some(JsonSchema::new(serde_json::json!({"type": "object"}))),
510            timeout: Some(Duration::from_secs(30)),
511            extra_headers: Some(http::HeaderMap::new()),
512            metadata: HashMap::from([("key".into(), serde_json::json!("val"))]),
513        };
514        assert_eq!(p.messages.len(), 1);
515        assert!(p.tools.is_some());
516        assert_eq!(p.temperature, Some(0.7));
517    }
518
519    // --- ToolChoice tests ---
520
521    #[test]
522    fn test_tool_choice_all_variants() {
523        let variants = [
524            ToolChoice::Auto,
525            ToolChoice::None,
526            ToolChoice::Required,
527            ToolChoice::Specific("my_tool".into()),
528        ];
529        for v in &variants {
530            assert_eq!(*v, v.clone());
531        }
532    }
533
534    #[test]
535    fn test_tool_choice_serde_roundtrip() {
536        let tc = ToolChoice::Specific("search".into());
537        let json = serde_json::to_string(&tc).unwrap();
538        let back: ToolChoice = serde_json::from_str(&json).unwrap();
539        assert_eq!(tc, back);
540    }
541
542    // --- JsonSchema tests ---
543
544    #[test]
545    fn test_json_schema_from_raw() {
546        let schema = JsonSchema::new(serde_json::json!({"type": "object"}));
547        assert_eq!(*schema.as_value(), serde_json::json!({"type": "object"}));
548    }
549
550    #[cfg(feature = "schema")]
551    #[test]
552    fn test_json_schema_from_type_simple() {
553        #[derive(schemars::JsonSchema)]
554        struct Foo {
555            #[allow(dead_code)]
556            x: i32,
557        }
558        let schema = JsonSchema::from_type::<Foo>().unwrap();
559        let props = schema
560            .as_value()
561            .get("properties")
562            .expect("should have properties");
563        assert!(props.get("x").is_some());
564    }
565
566    #[cfg(feature = "schema")]
567    #[test]
568    fn test_json_schema_validate_valid() {
569        let schema = JsonSchema::new(serde_json::json!({
570            "type": "object",
571            "properties": {
572                "x": {"type": "integer"}
573            },
574            "required": ["x"]
575        }));
576        assert!(schema.validate(&serde_json::json!({"x": 42})).is_ok());
577    }
578
579    #[cfg(feature = "schema")]
580    #[test]
581    fn test_json_schema_validate_missing_field() {
582        let schema = JsonSchema::new(serde_json::json!({
583            "type": "object",
584            "properties": {
585                "x": {"type": "integer"}
586            },
587            "required": ["x"]
588        }));
589        let result = schema.validate(&serde_json::json!({}));
590        assert!(result.is_err());
591        assert!(matches!(
592            result.unwrap_err(),
593            LlmError::SchemaValidation { .. }
594        ));
595    }
596
597    #[cfg(feature = "schema")]
598    #[test]
599    fn test_json_schema_validate_wrong_type() {
600        let schema = JsonSchema::new(serde_json::json!({
601            "type": "object",
602            "properties": {
603                "x": {"type": "integer"}
604            },
605            "required": ["x"]
606        }));
607        let result = schema.validate(&serde_json::json!({"x": "not a number"}));
608        assert!(result.is_err());
609    }
610
611    #[cfg(feature = "schema")]
612    #[test]
613    fn test_json_schema_validate_invalid_schema() {
614        let schema = JsonSchema::new(serde_json::json!({"type": "bogus_not_a_type"}));
615        let result = schema.validate(&serde_json::json!(42));
616        assert!(result.is_err());
617        assert!(matches!(result.unwrap_err(), LlmError::InvalidRequest(_)));
618    }
619
620    #[test]
621    fn test_json_schema_clone_eq() {
622        let s = JsonSchema::new(serde_json::json!({"type": "string"}));
623        assert_eq!(s, s.clone());
624    }
625
626    #[test]
627    fn test_json_schema_serde_roundtrip() {
628        let s = JsonSchema::new(
629            serde_json::json!({"type": "object", "properties": {"x": {"type": "integer"}}}),
630        );
631        let json = serde_json::to_string(&s).unwrap();
632        let back: JsonSchema = serde_json::from_str(&json).unwrap();
633        assert_eq!(s, back);
634    }
635
636    #[test]
637    fn test_tool_definition_serde_roundtrip() {
638        let td = ToolDefinition {
639            name: "search".into(),
640            description: "Search the web".into(),
641            parameters: JsonSchema::new(serde_json::json!({"type": "object"})),
642            retry: None,
643        };
644        let json = serde_json::to_string(&td).unwrap();
645        let back: ToolDefinition = serde_json::from_str(&json).unwrap();
646        assert_eq!(td, back);
647    }
648
649    #[test]
650    fn test_provider_metadata_serde_roundtrip() {
651        let m = ProviderMetadata {
652            name: "anthropic".into(),
653            model: "claude-sonnet-4".into(),
654            context_window: 200_000,
655            capabilities: HashSet::from([Capability::Tools, Capability::Vision]),
656        };
657        let json = serde_json::to_string(&m).unwrap();
658        let back: ProviderMetadata = serde_json::from_str(&json).unwrap();
659        assert_eq!(m, back);
660    }
661
662    #[test]
663    fn test_chat_params_serde_roundtrip_with_metadata() {
664        let p = ChatParams {
665            messages: vec![ChatMessage::user("hi")],
666            metadata: HashMap::from([
667                ("provider_key".into(), serde_json::json!("abc123")),
668                ("flags".into(), serde_json::json!({"stream": true})),
669            ]),
670            ..Default::default()
671        };
672        let json = serde_json::to_string(&p).unwrap();
673        let back: ChatParams = serde_json::from_str(&json).unwrap();
674        assert_eq!(back.metadata.len(), 2);
675        assert_eq!(back.metadata["provider_key"], serde_json::json!("abc123"));
676        assert_eq!(back.metadata["flags"], serde_json::json!({"stream": true}));
677    }
678
679    #[test]
680    fn test_chat_params_serde_roundtrip_skips_timeout_and_headers() {
681        let p = ChatParams {
682            messages: vec![ChatMessage::user("hi")],
683            temperature: Some(0.7),
684            timeout: Some(Duration::from_secs(30)),
685            extra_headers: Some(http::HeaderMap::new()),
686            ..Default::default()
687        };
688        let json = serde_json::to_string(&p).unwrap();
689        let back: ChatParams = serde_json::from_str(&json).unwrap();
690        // timeout and extra_headers are skipped
691        assert_eq!(back.timeout, None);
692        assert_eq!(back.extra_headers, None);
693        // other fields survive
694        assert_eq!(back.messages.len(), 1);
695        assert_eq!(back.temperature, Some(0.7));
696    }
697}