Skip to main content

openrouter_rs/types/
completion.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5
6#[derive(Serialize, Deserialize, Debug, Clone)]
7#[non_exhaustive]
8pub struct ReasoningDetail {
9    /// The type of reasoning block (e.g., "reasoning.text", "reasoning.encrypted")
10    #[serde(rename = "type")]
11    pub block_type: String,
12    /// The actual reasoning content (Anthropic uses "text" field)
13    #[serde(alias = "content", default)]
14    pub text: Option<String>,
15    /// Encrypted reasoning data (Gemini uses "data" field)
16    #[serde(default)]
17    pub data: Option<String>,
18    /// Cryptographic signature (Anthropic specific)
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub signature: Option<String>,
21    /// Format identifier
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub format: Option<String>,
24    /// ID of the reasoning block (Gemini specific)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub id: Option<String>,
27    /// Index of the reasoning block (Gemini specific)
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub index: Option<u32>,
30    /// Server-tool name for `reasoning.server_tool_call` blocks.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub tool_name: Option<String>,
33    /// Serialized server-tool arguments.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub arguments: Option<String>,
36    /// Server-tool result.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub result: Option<String>,
39    /// Tool-call identifier when supplied by the provider.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub tool_call_id: Option<String>,
42}
43
44impl ReasoningDetail {
45    /// Get the content/text of this reasoning detail
46    pub fn content(&self) -> Option<&str> {
47        self.text.as_deref().or(self.data.as_deref())
48    }
49
50    /// Get the type of this reasoning block
51    pub fn reasoning_type(&self) -> &str {
52        &self.block_type
53    }
54}
55
56#[derive(Serialize, Deserialize, Debug, Clone)]
57#[non_exhaustive]
58pub struct ResponseCostDetails {
59    /// Upstream provider cost attributed to completion tokens.
60    pub upstream_inference_completions_cost: f64,
61    /// Upstream provider cost attributed to prompt tokens.
62    pub upstream_inference_prompt_cost: f64,
63    /// Total upstream inference cost when provided separately by OpenRouter.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub upstream_inference_cost: Option<f64>,
66}
67
68#[derive(Serialize, Deserialize, Debug, Clone)]
69#[non_exhaustive]
70pub struct ServerToolUseDetails {
71    /// Number of OpenRouter server tool calls that executed and produced a result.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub tool_calls_executed: Option<u32>,
74    /// Number of OpenRouter server-orchestrated tool calls requested by the model.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub tool_calls_requested: Option<u32>,
77    /// Number of web searches performed by server-side tools.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub web_search_requests: Option<u32>,
80}
81
82/// Anthropic-style prompt-cache creation token breakdown.
83#[derive(Serialize, Deserialize, Debug, Clone)]
84#[non_exhaustive]
85pub struct AnthropicCacheCreation {
86    pub ephemeral_5m_input_tokens: u64,
87    pub ephemeral_1h_input_tokens: u64,
88}
89
90/// Token and billing usage reported for chat completion responses.
91///
92/// Response payloads are intentionally constructed by deserialization rather than
93/// by public struct literals, so new upstream usage fields can be typed without
94/// forcing caller source changes.
95///
96/// ```compile_fail
97/// use openrouter_rs::types::completion::ResponseUsage;
98///
99/// let usage = ResponseUsage {
100///     prompt_tokens: 1,
101///     completion_tokens: 2,
102///     total_tokens: 3,
103///     cost: None,
104///     cost_details: None,
105///     is_byok: None,
106/// };
107/// ```
108#[derive(Serialize, Deserialize, Debug, Clone)]
109#[non_exhaustive]
110pub struct ResponseUsage {
111    /// Including images and tools if any
112    pub prompt_tokens: u32,
113    /// The tokens generated
114    pub completion_tokens: u32,
115    /// Sum of the above two fields
116    pub total_tokens: u32,
117    /// Total OpenRouter request cost when returned by the API.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub cost: Option<f64>,
120    /// Provider-level cost breakdown when returned by the API.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub cost_details: Option<ResponseCostDetails>,
123    /// Whether the request was billed through BYOK provider credentials.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub is_byok: Option<bool>,
126    /// Server-side tool execution usage when returned by OpenRouter.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub server_tool_use_details: Option<ServerToolUseDetails>,
129}
130
131impl ResponseUsage {
132    pub fn new(prompt_tokens: u32, completion_tokens: u32, total_tokens: u32) -> Self {
133        Self {
134            prompt_tokens,
135            completion_tokens,
136            total_tokens,
137            cost: None,
138            cost_details: None,
139            is_byok: None,
140            server_tool_use_details: None,
141        }
142    }
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone)]
146#[non_exhaustive]
147pub struct FunctionCall {
148    pub name: String,
149    pub arguments: String,
150}
151
152impl FunctionCall {
153    pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
154        Self {
155            name: name.into(),
156            arguments: arguments.into(),
157        }
158    }
159}
160
161#[derive(Serialize, Deserialize, Debug, Clone)]
162#[non_exhaustive]
163pub struct ToolCall {
164    pub id: String,
165    #[serde(rename = "type")]
166    pub type_: String, // Always "function" according to TS type
167    pub function: FunctionCall,
168    #[serde(default)]
169    pub index: Option<u32>,
170}
171
172impl ToolCall {
173    pub fn new(
174        id: impl Into<String>,
175        name: impl Into<String>,
176        arguments: impl Into<String>,
177    ) -> Self {
178        Self {
179            id: id.into(),
180            type_: "function".to_string(),
181            function: FunctionCall::new(name, arguments),
182            index: None,
183        }
184    }
185
186    pub fn with_index(mut self, index: u32) -> Self {
187        self.index = Some(index);
188        self
189    }
190}
191
192/// Partial function call data as received in streaming deltas.
193///
194/// Unlike [`FunctionCall`], all fields are optional because streaming chunks
195/// may only contain fragments of the function call (e.g., just an arguments
196/// fragment without the function name).
197#[derive(Serialize, Deserialize, Debug, Clone, Default)]
198#[non_exhaustive]
199pub struct PartialFunctionCall {
200    #[serde(default)]
201    pub name: Option<String>,
202    #[serde(default)]
203    pub arguments: Option<String>,
204}
205
206/// Partial tool call data as received in streaming deltas.
207///
208/// When the API streams a response that includes tool calls, each SSE chunk
209/// contains only a fragment of the tool call data. The first chunk typically
210/// contains `id`, `type`, and the function `name`, while subsequent chunks
211/// contain fragments of the `arguments` string.
212///
213/// Use [`ToolAwareStream`](crate::types::stream::ToolAwareStream) to
214/// automatically accumulate these partial chunks into complete [`ToolCall`]
215/// objects.
216#[derive(Serialize, Deserialize, Debug, Clone, Default)]
217#[non_exhaustive]
218pub struct PartialToolCall {
219    #[serde(default)]
220    pub id: Option<String>,
221    #[serde(default, rename = "type")]
222    pub type_: Option<String>,
223    #[serde(default)]
224    pub function: Option<PartialFunctionCall>,
225    #[serde(default)]
226    pub index: Option<u32>,
227}
228
229impl ToolCall {
230    /// Parse tool arguments into typed parameters
231    ///
232    /// # Examples
233    ///
234    /// ```rust
235    /// use openrouter_rs::types::ToolCall;
236    /// use openrouter_rs::types::typed_tool::TypedTool;
237    /// use serde::{Deserialize, Serialize};
238    /// use schemars::JsonSchema;
239    ///
240    /// #[derive(Serialize, Deserialize, JsonSchema)]
241    /// struct WeatherParams {
242    ///     location: String,
243    /// }
244    ///
245    /// impl TypedTool for WeatherParams {
246    ///     fn name() -> &'static str { "get_weather" }
247    ///     fn description() -> &'static str { "Get weather" }
248    /// }
249    ///
250    /// let tool_call = ToolCall::new("call_123", "get_weather", r#"{"location":"Paris"}"#);
251    ///
252    /// let params: WeatherParams = tool_call.parse_params()?;
253    /// # Ok::<(), openrouter_rs::error::OpenRouterError>(())
254    /// ```
255    pub fn parse_params<T>(&self) -> Result<T, crate::error::OpenRouterError>
256    where
257        T: crate::types::typed_tool::TypedTool,
258    {
259        serde_json::from_str(&self.function.arguments)
260            .map_err(crate::error::OpenRouterError::Serialization)
261    }
262
263    /// Check if this tool call matches a specific tool type
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// # use openrouter_rs::types::ToolCall;
269    /// # use openrouter_rs::types::typed_tool::TypedTool;
270    /// # use serde::{Deserialize, Serialize};
271    /// # use schemars::JsonSchema;
272    /// # #[derive(Serialize, Deserialize, JsonSchema)]
273    /// # struct WeatherParams { location: String }
274    /// # impl TypedTool for WeatherParams {
275    /// #     fn name() -> &'static str { "get_weather" }
276    /// #     fn description() -> &'static str { "Get weather" }
277    /// # }
278    /// # let tool_call = ToolCall::new("call_123", "get_weather", r#"{"location":"Paris"}"#);
279    /// if tool_call.is_tool::<WeatherParams>() {
280    ///     let params = tool_call.parse_params::<WeatherParams>()?;
281    ///     // Handle weather tool
282    /// }
283    /// # Ok::<(), openrouter_rs::error::OpenRouterError>(())
284    /// ```
285    pub fn is_tool<T>(&self) -> bool
286    where
287        T: crate::types::typed_tool::TypedTool,
288    {
289        self.function.name == T::name()
290    }
291
292    /// Get the tool name
293    ///
294    /// # Examples
295    ///
296    /// ```rust
297    /// # use openrouter_rs::types::ToolCall;
298    /// # let tool_call = ToolCall::new("call_123", "get_weather", "{}");
299    /// match tool_call.name() {
300    ///     "get_weather" => { /* handle weather */ }
301    ///     "calculator" => { /* handle calculator */ }
302    ///     _ => { /* unknown tool */ }
303    /// }
304    /// ```
305    pub fn name(&self) -> &str {
306        &self.function.name
307    }
308
309    /// Get the raw JSON arguments as a string
310    ///
311    /// # Examples
312    ///
313    /// ```rust
314    /// # use openrouter_rs::types::ToolCall;
315    /// # let tool_call = ToolCall::new("call_123", "get_weather", r#"{"location":"Paris"}"#);
316    /// println!("Raw arguments: {}", tool_call.arguments_json());
317    /// ```
318    pub fn arguments_json(&self) -> &str {
319        &self.function.arguments
320    }
321
322    /// Get the tool call ID
323    ///
324    /// # Examples
325    ///
326    /// ```rust
327    /// # use openrouter_rs::types::ToolCall;
328    /// # let tool_call = ToolCall::new("call_123", "get_weather", "{}");
329    /// println!("Tool call ID: {}", tool_call.id());
330    /// ```
331    pub fn id(&self) -> &str {
332        &self.id
333    }
334
335    /// Get the tool type (usually "function")
336    ///
337    /// # Examples
338    ///
339    /// ```rust
340    /// # use openrouter_rs::types::ToolCall;
341    /// # let tool_call = ToolCall::new("call_123", "get_weather", "{}");
342    /// assert_eq!(tool_call.tool_type(), "function");
343    /// ```
344    pub fn tool_type(&self) -> &str {
345        &self.type_
346    }
347}
348
349#[derive(Serialize, Deserialize, Debug, Clone)]
350#[non_exhaustive]
351pub struct ErrorResponse {
352    pub code: i32,
353    pub message: String,
354    pub metadata: Option<HashMap<String, Value>>,
355}
356
357fn extract_text_from_content_value(value: &Value) -> Option<String> {
358    match value {
359        Value::Null => None,
360        Value::String(text) => Some(text.clone()),
361        Value::Object(part) => extract_text_from_content_part(part),
362        Value::Array(parts) => {
363            let text = parts
364                .iter()
365                .filter_map(|part| match part {
366                    Value::Object(part) => extract_text_from_content_part(part),
367                    _ => None,
368                })
369                .collect::<String>();
370
371            (!text.is_empty()).then_some(text)
372        }
373        _ => None,
374    }
375}
376
377fn extract_text_from_content_part(part: &serde_json::Map<String, Value>) -> Option<String> {
378    let part_type = part.get("type").and_then(Value::as_str);
379    if let Some(kind) = part_type {
380        if !matches!(kind, "text" | "output_text" | "input_text") {
381            return None;
382        }
383    }
384
385    part.get("text")
386        .and_then(Value::as_str)
387        .or_else(|| part.get("content").and_then(Value::as_str))
388        .map(ToString::to_string)
389}
390
391fn deserialize_optional_text_content<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
392where
393    D: Deserializer<'de>,
394{
395    let value = Option::<Value>::deserialize(deserializer)?;
396    Ok(value.as_ref().and_then(extract_text_from_content_value))
397}
398
399#[derive(Serialize, Deserialize, Debug, Clone)]
400#[non_exhaustive]
401#[serde(untagged)]
402pub enum Choice {
403    NonChat(NonChatChoice),
404    NonStreaming(NonStreamingChoice),
405    Streaming(StreamingChoice),
406}
407
408impl Choice {
409    pub fn content(&self) -> Option<&str> {
410        match self {
411            Choice::NonChat(choice) => Some(choice.text.as_str()),
412            Choice::NonStreaming(choice) => choice.message.content.as_deref(),
413            Choice::Streaming(choice) => choice.delta.content.as_deref(),
414        }
415    }
416
417    pub fn role(&self) -> Option<&str> {
418        match self {
419            Choice::NonChat(_) => None,
420            Choice::NonStreaming(choice) => choice.message.role.as_deref(),
421            Choice::Streaming(choice) => choice.delta.role.as_deref(),
422        }
423    }
424
425    /// Returns the complete tool calls for non-streaming responses.
426    ///
427    /// For streaming responses, this always returns `None` because tool calls
428    /// arrive as partial fragments across multiple chunks. Use
429    /// [`ToolAwareStream`](crate::types::stream::ToolAwareStream) to
430    /// accumulate streaming tool call fragments into complete [`ToolCall`] objects.
431    pub fn tool_calls(&self) -> Option<&[ToolCall]> {
432        match self {
433            Choice::NonChat(_) => None,
434            Choice::NonStreaming(choice) => choice.message.tool_calls.as_deref(),
435            Choice::Streaming(_) => None,
436        }
437    }
438
439    /// Returns the partial tool call fragments from a streaming delta.
440    ///
441    /// This is only populated for streaming responses. Each chunk contains
442    /// a fragment of the tool call data that must be accumulated across
443    /// the entire stream to form complete tool calls.
444    ///
445    /// For most use cases, prefer [`ToolAwareStream`](crate::types::stream::ToolAwareStream)
446    /// which handles this accumulation automatically.
447    pub fn partial_tool_calls(&self) -> Option<&[PartialToolCall]> {
448        match self {
449            Choice::NonChat(_) => None,
450            Choice::NonStreaming(_) => None,
451            Choice::Streaming(choice) => choice.delta.tool_calls.as_deref(),
452        }
453    }
454
455    pub fn finish_reason(&self) -> Option<&FinishReason> {
456        match self {
457            Choice::NonChat(choice) => choice.finish_reason.as_ref(),
458            Choice::NonStreaming(choice) => choice.finish_reason.as_ref(),
459            Choice::Streaming(choice) => choice.finish_reason.as_ref(),
460        }
461    }
462
463    pub fn native_finish_reason(&self) -> Option<&str> {
464        match self {
465            Choice::NonChat(_) => None,
466            Choice::NonStreaming(choice) => choice.native_finish_reason.as_deref(),
467            Choice::Streaming(choice) => choice.native_finish_reason.as_deref(),
468        }
469    }
470
471    pub fn error(&self) -> Option<&ErrorResponse> {
472        match self {
473            Choice::NonChat(choice) => choice.error.as_ref(),
474            Choice::NonStreaming(choice) => choice.error.as_ref(),
475            Choice::Streaming(choice) => choice.error.as_ref(),
476        }
477    }
478
479    pub fn index(&self) -> Option<u32> {
480        match self {
481            Choice::NonChat(choice) => choice.index,
482            Choice::NonStreaming(choice) => choice.index,
483            Choice::Streaming(choice) => choice.index,
484        }
485    }
486
487    pub fn reasoning(&self) -> Option<&str> {
488        match self {
489            Choice::NonChat(_) => None,
490            Choice::NonStreaming(choice) => choice.message.reasoning.as_deref(),
491            Choice::Streaming(choice) => choice.delta.reasoning.as_deref(),
492        }
493    }
494
495    pub fn reasoning_details(&self) -> Option<&[ReasoningDetail]> {
496        match self {
497            Choice::NonChat(_) => None,
498            Choice::NonStreaming(choice) => choice.message.reasoning_details.as_deref(),
499            Choice::Streaming(choice) => choice.delta.reasoning_details.as_deref(),
500        }
501    }
502
503    pub fn logprobs(&self) -> Option<&Value> {
504        match self {
505            Choice::NonChat(choice) => choice.logprobs.as_ref(),
506            Choice::NonStreaming(choice) => choice.logprobs.as_ref(),
507            Choice::Streaming(choice) => choice.logprobs.as_ref(),
508        }
509    }
510}
511
512/// Why the model stopped generating.
513///
514/// OpenRouter's wire schema marks `finish_reason` as allowing unknown values
515/// (`x-speakeasy-unknown-values: allow`), so providers may stream values this
516/// SDK does not model (e.g. a legacy `"function_call"`). Deserializing such a
517/// value into a strict enum would fail the entire SSE frame, dropping its
518/// content — so unrecognized values are captured verbatim in
519/// [`FinishReason::Other`] instead.
520#[derive(Debug, Clone)]
521#[non_exhaustive]
522pub enum FinishReason {
523    ToolCalls,
524    Stop,
525    Length,
526    ContentFilter,
527    Error,
528    /// An upstream finish-reason value not yet modeled by this SDK.
529    ///
530    /// The raw wire string is preserved exactly as received.
531    Other(String),
532}
533
534impl FinishReason {
535    /// The wire-string form of this finish reason.
536    fn as_str(&self) -> &str {
537        match self {
538            FinishReason::ToolCalls => "tool_calls",
539            FinishReason::Stop => "stop",
540            FinishReason::Length => "length",
541            FinishReason::ContentFilter => "content_filter",
542            FinishReason::Error => "error",
543            FinishReason::Other(value) => value,
544        }
545    }
546}
547
548impl Serialize for FinishReason {
549    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
550        serializer.serialize_str(self.as_str())
551    }
552}
553
554impl<'de> Deserialize<'de> for FinishReason {
555    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
556        struct Visitor;
557
558        impl<'de> serde::de::Visitor<'de> for Visitor {
559            type Value = FinishReason;
560
561            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
562                formatter.write_str("a finish reason string")
563            }
564
565            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
566                Ok(match value {
567                    "tool_calls" => FinishReason::ToolCalls,
568                    "stop" => FinishReason::Stop,
569                    "length" => FinishReason::Length,
570                    "content_filter" => FinishReason::ContentFilter,
571                    "error" => FinishReason::Error,
572                    other => FinishReason::Other(other.to_string()),
573                })
574            }
575        }
576
577        deserializer.deserialize_str(Visitor)
578    }
579}
580
581#[derive(Serialize, Deserialize, Debug, Clone)]
582#[non_exhaustive]
583pub struct NonChatChoice {
584    pub finish_reason: Option<FinishReason>,
585    pub text: String,
586    pub error: Option<ErrorResponse>,
587    pub index: Option<u32>,
588    pub logprobs: Option<Value>,
589}
590
591#[derive(Serialize, Deserialize, Debug, Clone)]
592#[non_exhaustive]
593pub struct NonStreamingChoice {
594    pub finish_reason: Option<FinishReason>,
595    pub native_finish_reason: Option<String>,
596    pub message: Message,
597    pub error: Option<ErrorResponse>,
598    pub index: Option<u32>,
599    pub logprobs: Option<Value>,
600}
601
602#[derive(Serialize, Deserialize, Debug, Clone)]
603#[non_exhaustive]
604pub struct StreamingChoice {
605    pub finish_reason: Option<FinishReason>,
606    pub native_finish_reason: Option<String>,
607    pub delta: Delta,
608    pub error: Option<ErrorResponse>,
609    pub index: Option<u32>,
610    pub logprobs: Option<Value>,
611}
612
613#[derive(Serialize, Deserialize, Debug, Clone)]
614#[non_exhaustive]
615pub struct Message {
616    #[serde(default, deserialize_with = "deserialize_optional_text_content")]
617    pub content: Option<String>,
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub model: Option<String>,
620    pub role: Option<String>,
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub name: Option<String>,
623    pub tool_calls: Option<Vec<ToolCall>>,
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub reasoning: Option<String>,
626    #[serde(skip_serializing_if = "Option::is_none")]
627    pub reasoning_details: Option<Vec<ReasoningDetail>>,
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub images: Option<Vec<Value>>,
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub audio: Option<Value>,
632    pub refusal: Option<String>,
633    #[serde(default)]
634    pub annotations: Option<Vec<Value>>,
635}
636
637#[derive(Serialize, Deserialize, Debug, Clone)]
638#[non_exhaustive]
639pub struct Delta {
640    #[serde(default, deserialize_with = "deserialize_optional_text_content")]
641    pub content: Option<String>,
642    pub role: Option<String>,
643    /// Partial tool call fragments received during streaming.
644    ///
645    /// Each chunk contains only a fragment of the full tool call data.
646    /// Use [`ToolAwareStream`](crate::types::stream::ToolAwareStream)
647    /// to accumulate these into complete [`ToolCall`] objects.
648    pub tool_calls: Option<Vec<PartialToolCall>>,
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub reasoning: Option<String>,
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub reasoning_details: Option<Vec<ReasoningDetail>>,
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub audio: Option<Value>,
655    pub refusal: Option<String>,
656}
657
658#[derive(Serialize, Deserialize, Debug, Clone)]
659#[non_exhaustive]
660pub enum ObjectType {
661    #[serde(rename = "chat.completion")]
662    ChatCompletion,
663    #[serde(rename = "chat.completion.chunk")]
664    ChatCompletionChunk,
665}
666
667#[derive(Serialize, Deserialize, Debug, Clone)]
668#[non_exhaustive]
669pub struct CompletionsResponse {
670    pub id: String,
671    pub choices: Vec<Choice>,
672    pub created: u64, // Unix timestamp
673    pub model: String,
674    #[serde(rename = "object")]
675    pub object_type: ObjectType,
676    pub provider: Option<String>,
677    pub system_fingerprint: Option<String>,
678    pub usage: Option<ResponseUsage>,
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub service_tier: Option<String>,
681    #[serde(default, skip_serializing_if = "Option::is_none")]
682    pub openrouter_metadata: Option<Value>,
683}