Skip to main content

switchyard_translation/codecs/responses/
mod.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! OpenAI Responses buffered and streaming codecs.
5
6mod buffered;
7mod stream;
8
9pub use buffered::OpenAiResponsesCodec;
10pub use stream::OpenAiResponsesStreamCodec;
11
12pub(crate) fn is_native_output(kind: &str) -> bool {
13    matches!(
14        kind,
15        "apply_patch_call" | "shell_call" | "computer_call" | "image_generation_call"
16    )
17}
18
19pub(crate) fn validate_response_output(
20    response: &crate::AggLlmResponse,
21    target: crate::WireFormat,
22) -> crate::Result<()> {
23    for block in response.outputs.iter().flat_map(|output| &output.content) {
24        if let crate::ContentBlock::Unknown { provider, raw } = block
25            && provider.as_str() == crate::WireFormat::OpenAiResponses.as_str()
26            && raw["type"].as_str().is_some_and(is_native_output)
27        {
28            return Err(crate::TranslationError::UnsupportedTranslation {
29                from: provider.clone(),
30                to: target.into(),
31            });
32        }
33    }
34    Ok(())
35}
36
37// Native outputs have no neutral streaming representation; inspect them before discarding raw events.
38pub(crate) fn validate_stream_output(
39    source: &crate::FormatId,
40    target: &crate::FormatId,
41    event: &serde_json::Value,
42) -> crate::Result<()> {
43    if source.as_str() == crate::WireFormat::OpenAiResponses.as_str() && source != target {
44        let mut items = event
45            .get("item")
46            .into_iter()
47            .chain(event["response"]["output"].as_array().into_iter().flatten());
48        if items.any(|item| item["type"].as_str().is_some_and(is_native_output)) {
49            return Err(crate::TranslationError::UnsupportedTranslation {
50                from: source.clone(),
51                to: target.clone(),
52            });
53        }
54    }
55    Ok(())
56}
57
58// This is a transport envelope, not encryption. Keep it distinct from native OpenAI
59// ciphertext, and retain the original text because the signature covers that text.
60const ANTHROPIC_THINKING_PREFIX: &str = "switchyard:anthropic-thinking:v1:";
61
62fn encode_anthropic_thinking(text: &str, signature: &str) -> String {
63    format!(
64        "{ANTHROPIC_THINKING_PREFIX}{}",
65        serde_json::json!([text, signature])
66    )
67}
68
69fn decode_anthropic_thinking(payload: &str) -> Option<(String, String)> {
70    serde_json::from_str(payload.strip_prefix(ANTHROPIC_THINKING_PREFIX)?).ok()
71}
72
73/// Returns true when a Responses `encrypted_content` value holds Anthropic thinking that
74/// Switchyard packed for a Responses client, rather than ciphertext from an OpenAI provider.
75/// Only Switchyard can read this value, so an OpenAI provider cannot decrypt it.
76pub fn is_anthropic_thinking_envelope(encrypted_content: &str) -> bool {
77    encrypted_content.starts_with(ANTHROPIC_THINKING_PREFIX)
78}