Skip to main content

gproxy_protocol/wire/openai/chat/
reasoning.rs

1//! DeepSeek reasoning_content and OpenRouter reasoning/reasoning_details.
2//! Source: upstream_docs/openrouter/Create a chat completion.md, ReasoningDetailUnion.
3use super::content::present_nullable;
4use crate::Rest;
5use serde::{Deserialize, Serialize};
6
7#[derive(
8    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, gproxy_protocol_macros::DeclaredFields,
9)]
10pub enum ReasoningDetailKind {
11    #[serde(rename = "reasoning.text")]
12    Text,
13    #[serde(rename = "reasoning.summary")]
14    Summary,
15    #[serde(rename = "reasoning.encrypted")]
16    Encrypted,
17}
18/// Payload fields are optional because streaming detail fragments may carry
19/// only a signature or identity. Encrypted data is never interpreted as text.
20#[derive(
21    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, gproxy_protocol_macros::DeclaredFields,
22)]
23pub struct ReasoningDetail {
24    #[serde(rename = "type")]
25    pub type_: ReasoningDetailKind,
26    #[serde(
27        default,
28        deserialize_with = "present_nullable",
29        skip_serializing_if = "Option::is_none"
30    )]
31    pub text: Option<Option<String>>,
32    #[serde(
33        default,
34        deserialize_with = "present_nullable",
35        skip_serializing_if = "Option::is_none"
36    )]
37    pub signature: Option<Option<String>>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub summary: Option<String>,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub data: Option<String>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub format: Option<String>,
44    #[serde(
45        default,
46        deserialize_with = "present_nullable",
47        skip_serializing_if = "Option::is_none"
48    )]
49    pub id: Option<Option<String>>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub index: Option<i64>,
52    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
53    pub rest: Rest,
54}
55
56pub fn visible_reasoning(
57    content: &Option<Option<String>>,
58    reasoning: &Option<Option<String>>,
59    details: &Option<Option<Vec<ReasoningDetail>>>,
60) -> Option<String> {
61    if let Some(text) = super::reasoning_text(content, reasoning).filter(|s| !s.is_empty()) {
62        return Some(text.into());
63    }
64    let texts: Vec<_> = details
65        .as_ref()
66        .and_then(Option::as_ref)
67        .into_iter()
68        .flatten()
69        .filter_map(|d| match d.type_ {
70            ReasoningDetailKind::Text => d.text.as_ref().and_then(Option::as_deref),
71            ReasoningDetailKind::Summary => d.summary.as_deref(),
72            ReasoningDetailKind::Encrypted => None,
73        })
74        .collect();
75    if texts.is_empty() {
76        super::reasoning_text(content, reasoning).map(str::to_owned)
77    } else {
78        Some(texts.join(""))
79    }
80}