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