use std::{fmt, str::FromStr};
use serde::{Serialize, Serializer, ser::Error as _};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningField {
Reasoning,
ReasoningContent,
}
impl ReasoningField {
pub const DEFAULT: Self = Self::ReasoningContent;
pub fn as_str(self) -> &'static str {
match self {
Self::Reasoning => "reasoning",
Self::ReasoningContent => "reasoning_content",
}
}
}
impl Default for ReasoningField {
fn default() -> Self {
Self::DEFAULT
}
}
impl fmt::Display for ReasoningField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReasoningFieldParseError {
value: String,
}
impl fmt::Display for ReasoningFieldParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"reasoning field name {:?} is not \"reasoning\" or \"reasoning_content\"",
self.value
)
}
}
impl std::error::Error for ReasoningFieldParseError {}
impl FromStr for ReasoningField {
type Err = ReasoningFieldParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"reasoning" => Ok(Self::Reasoning),
"reasoning_content" => Ok(Self::ReasoningContent),
other => Err(ReasoningFieldParseError {
value: other.to_string(),
}),
}
}
}
#[derive(Debug, Clone)]
pub struct RoutedReasoning<T> {
inner: T,
field: ReasoningField,
}
impl<T> RoutedReasoning<T> {
pub fn new(inner: T, field: ReasoningField) -> Self {
Self { inner, field }
}
}
impl<T: Serialize> Serialize for RoutedReasoning<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.field == ReasoningField::ReasoningContent {
return self.inner.serialize(serializer);
}
let mut value = serde_json::to_value(&self.inner).map_err(S::Error::custom)?;
route_serialized_reasoning(&mut value, self.field);
value.serialize(serializer)
}
}
fn route_serialized_reasoning(value: &mut Value, field: ReasoningField) {
if field == ReasoningField::ReasoningContent {
return;
}
let Some(choices) = value.get_mut("choices").and_then(Value::as_array_mut) else {
return;
};
for choice in choices {
let Some(choice) = choice.as_object_mut() else {
continue;
};
for container_name in ["message", "delta"] {
let Some(container) = choice
.get_mut(container_name)
.and_then(Value::as_object_mut)
else {
continue;
};
if let Some(reasoning) = container.remove("reasoning_content") {
container.insert("reasoning".to_string(), reasoning);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn routes_unary_and_streaming_reasoning() {
let mut unary = json!({
"choices": [{
"message": {"content": null, "reasoning_content": "thinking"}
}]
});
route_serialized_reasoning(&mut unary, ReasoningField::Reasoning);
assert_eq!(unary["choices"][0]["message"]["reasoning"], "thinking");
assert!(
unary["choices"][0]["message"]
.get("reasoning_content")
.is_none()
);
let mut stream = json!({
"choices": [{
"delta": {"reasoning_content": "think"}
}]
});
route_serialized_reasoning(&mut stream, ReasoningField::Reasoning);
assert_eq!(stream["choices"][0]["delta"]["reasoning"], "think");
assert!(
stream["choices"][0]["delta"]
.get("reasoning_content")
.is_none()
);
}
#[test]
fn reasoning_content_mode_preserves_upstream_json() {
let mut value = json!({
"choices": [{
"message": {"reasoning_content": "thinking"}
}],
"nvext": {"reasoning_content": "unrelated"}
});
let original = value.clone();
route_serialized_reasoning(&mut value, ReasoningField::ReasoningContent);
assert_eq!(value, original);
}
#[test]
fn routing_does_not_rewrite_extension_fields_or_add_missing_fields() {
let mut value = json!({
"choices": [{"delta": {"content": "answer"}}],
"nvext": {"reasoning_content": "extension metadata"}
});
route_serialized_reasoning(&mut value, ReasoningField::Reasoning);
assert!(value["choices"][0]["delta"].get("reasoning").is_none());
assert_eq!(value["nvext"]["reasoning_content"], "extension metadata");
}
}