Skip to main content

switchyard_llm_client/
responses_reasoning.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Target-specific replay policy for OpenAI Responses reasoning items.
5
6use serde::Deserialize;
7use serde_json::Value;
8
9/// Controls which Responses reasoning items are replayed to an upstream.
10#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
11#[serde(rename_all = "snake_case")]
12pub enum ResponsesReasoningPolicy {
13    /// Preserve encrypted reasoning and stored IDs the provider can read.
14    ///
15    /// This is the default for every Responses model, regardless of its URL.
16    #[default]
17    PreserveEncrypted,
18    /// Drop all reasoning items while preserving messages and tool-call history.
19    ///
20    /// Use this for local Responses-compatible servers that cannot consume
21    /// another provider's encrypted reasoning representation.
22    Drop,
23}
24
25impl ResponsesReasoningPolicy {
26    /// Normalizes a Responses request body for this replay policy.
27    pub(crate) fn normalize(self, body: &mut Value) {
28        let has_stored_reasoning = body.get("store").and_then(Value::as_bool) != Some(false);
29        let Some(Value::Array(input)) = body.get_mut("input") else {
30            return;
31        };
32        input.retain_mut(|item| self.normalize_item(item, has_stored_reasoning));
33    }
34
35    // Stored reasoning can be replayed by ID without encrypted content.
36    fn normalize_item(self, item: &mut Value, has_stored_reasoning: bool) -> bool {
37        let Some(object) = item.as_object_mut() else {
38            return true;
39        };
40        if object.get("type").and_then(Value::as_str) != Some("reasoning") {
41            return true;
42        }
43
44        let encrypted_content = object.get("encrypted_content").and_then(Value::as_str);
45        if encrypted_content.is_some_and(switchyard_translation::is_anthropic_thinking_envelope) {
46            return false;
47        }
48        let has_encrypted_content = encrypted_content.is_some_and(|content| !content.is_empty());
49        let has_stored_id = object
50            .get("id")
51            .and_then(Value::as_str)
52            .is_some_and(|id| !id.is_empty());
53        if self == Self::PreserveEncrypted
54            && (has_encrypted_content || (has_stored_reasoning && has_stored_id))
55        {
56            object.insert("content".to_string(), Value::Array(Vec::new()));
57            true
58        } else {
59            false
60        }
61    }
62}