Skip to content

Commit 0de2d55

Browse files
linj-glitchclaude
andcommitted
fix(translation): accept a verbatim reasoning item as an encrypted reasoning detail
The buffered request decoder in #645 keeps a provider's reasoning item whole as the reasoning detail when it carries encrypted_content. The stream and buffered response encoders here read the payload and item id through the shared helpers, so those helpers now recognise that shape alongside the documented reasoning.encrypted object. This keeps the buffered-decode, re-stream path (used by any route that buffers a reply) carrying the encrypted payload under its original id regardless of which PR lands first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
1 parent cc5a4c9 commit 0de2d55

1 file changed

Lines changed: 47 additions & 4 deletions

File tree

  • crates/switchyard-translation/src/codecs

crates/switchyard-translation/src/codecs/common.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ pub(crate) fn collect_responses_reasoning_text(value: Option<&Value>, out: &mut
8989
}
9090
}
9191

92-
/// Returns the opaque payload of the first `reasoning.encrypted` detail, if any.
92+
/// Returns the opaque payload of the first encrypted reasoning detail, if any.
93+
///
94+
/// Two detail shapes are accepted: the documented `{"type": "reasoning.encrypted", "data"}`
95+
/// object, and a verbatim Responses `reasoning` item carrying `encrypted_content` (the shape
96+
/// the buffered request decoder stores when it keeps the provider item whole).
9397
pub(crate) fn encrypted_reasoning_data(details: &[Value]) -> Option<String> {
9498
details
9599
.iter()
96100
.filter_map(Value::as_object)
97-
.find(|detail| detail.get("type").and_then(Value::as_str) == Some("reasoning.encrypted"))
98-
.and_then(|detail| detail.get("data").and_then(Value::as_str))
101+
.find_map(|detail| match detail.get("type").and_then(Value::as_str) {
102+
Some("reasoning.encrypted") => detail.get("data").and_then(Value::as_str),
103+
Some("reasoning") => detail.get("encrypted_content").and_then(Value::as_str),
104+
_ => None,
105+
})
99106
.filter(|data| !data.is_empty())
100107
.map(ToOwned::to_owned)
101108
}
@@ -106,7 +113,12 @@ pub(crate) fn encrypted_reasoning_item_id(details: &[Value]) -> Option<String> {
106113
details
107114
.iter()
108115
.filter_map(Value::as_object)
109-
.find(|detail| detail.get("type").and_then(Value::as_str) == Some("reasoning.encrypted"))
116+
.find(|detail| {
117+
matches!(
118+
detail.get("type").and_then(Value::as_str),
119+
Some("reasoning.encrypted" | "reasoning")
120+
)
121+
})
110122
.and_then(|detail| detail.get("id").and_then(Value::as_str))
111123
.filter(|id| !id.is_empty())
112124
.map(ToOwned::to_owned)
@@ -138,3 +150,34 @@ pub(crate) fn provider_extensions(
138150
}
139151
extensions
140152
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
use serde_json::json;
158+
159+
#[test]
160+
fn encrypted_reasoning_helpers_accept_both_detail_shapes() {
161+
let documented = vec![json!({"type": "reasoning.encrypted", "data": "blob", "id": "rs_1"})];
162+
assert_eq!(
163+
encrypted_reasoning_data(&documented).as_deref(),
164+
Some("blob")
165+
);
166+
assert_eq!(
167+
encrypted_reasoning_item_id(&documented).as_deref(),
168+
Some("rs_1")
169+
);
170+
let verbatim_item = vec![json!({
171+
"type": "reasoning", "id": "rs_2", "summary": [], "encrypted_content": "blob2"
172+
})];
173+
assert_eq!(
174+
encrypted_reasoning_data(&verbatim_item).as_deref(),
175+
Some("blob2")
176+
);
177+
assert_eq!(
178+
encrypted_reasoning_item_id(&verbatim_item).as_deref(),
179+
Some("rs_2")
180+
);
181+
assert_eq!(encrypted_reasoning_data(&[json!({"type": "other"})]), None);
182+
}
183+
}

0 commit comments

Comments
 (0)