Skip to main content

switchyard_translation/
request_edit.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Request edits update normalized content and preserved provider bodies together.
5
6use serde_json::{Map, Value, json};
7use switchyard_protocol::{ContentBlock, InstructionBlock, LlmRequest, Message, Role, WireFormat};
8
9/// This function appends temporary guidance and updates preserved provider bodies.
10/// It removes bodies it cannot edit so encoding rebuilds them from the updated request.
11pub fn append_note(request: &mut LlmRequest, note: &str) {
12    // A separate message preserves the caller's final message boundary for OpenAI caching.
13    request.messages.push(Message::text(Role::User, note));
14    request.temporary_note_count += 1;
15    request.preservation.requests.retain(|format, body| {
16        let Some(body) = body.as_object_mut() else {
17            return false;
18        };
19        let (key, content_type) = match format.as_str() {
20            "openai_chat" | "anthropic_messages" => ("messages", "text"),
21            "openai_responses" => ("input", "input_text"),
22            _ => return false,
23        };
24        // Responses also accepts a string as a single user input.
25        if key == "input"
26            && let Some(Value::String(text)) = body.get_mut(key)
27        {
28            let original = std::mem::take(text);
29            body.insert(key.into(), json!([{"role":"user", "content":original}]));
30        }
31        if key == "input" && body.get(key).is_none_or(Value::is_null) {
32            body.insert(key.into(), Value::Array(Vec::new()));
33        }
34        let Some(messages) = body.get_mut(key).and_then(Value::as_array_mut) else {
35            return false;
36        };
37        if format.as_str() == WireFormat::AnthropicMessages.as_str()
38            && let Some(last) = messages.last_mut()
39            && last.get("role").and_then(Value::as_str) == Some("user")
40        {
41            // Anthropic guidance follows existing content, including any tool result.
42            match last.get_mut("content") {
43                Some(Value::String(text)) => {
44                    let original = std::mem::take(text);
45                    last["content"] = json!([
46                        {"type":"text", "text":original},
47                        {"type":"text", "text":note}
48                    ]);
49                }
50                Some(Value::Array(blocks)) => blocks.push(json!({"type":"text", "text":note})),
51                _ => return false,
52            }
53        } else {
54            messages.push(json!({"role":"user", "content":[{"type":content_type,"text":note}]}));
55        }
56        true
57    });
58}
59
60/// This function prepends a system prompt and updates supported preserved provider bodies.
61/// It removes bodies it cannot edit so encoding rebuilds them from the updated request.
62pub fn prepend_system_prompt(request: &mut LlmRequest, prompt: &str) {
63    request.instructions.insert(
64        0,
65        InstructionBlock {
66            role: Role::System,
67            content: vec![ContentBlock::Text {
68                text: prompt.to_string(),
69            }],
70        },
71    );
72    request.preservation.requests.retain(|format, body| {
73        let Some(body) = body.as_object_mut() else {
74            return false;
75        };
76        match format.as_str() {
77            "openai_chat" => prepend_openai_chat_system(body, prompt),
78            "openai_responses" => prepend_text_field(body, "instructions", prompt),
79            "anthropic_messages" => {
80                body.remove("switchyard_anthropic_request");
81                prepend_anthropic_system(body, prompt)
82            }
83            _ => false,
84        }
85    });
86}
87
88// This helper inserts a system message while keeping the existing Chat messages intact.
89fn prepend_openai_chat_system(body: &mut Map<String, Value>, prompt: &str) -> bool {
90    let message = json!({"role": "system", "content": prompt});
91    match body.get_mut("messages") {
92        None | Some(Value::Null) => {
93            body.insert("messages".to_string(), Value::Array(vec![message]));
94        }
95        Some(Value::Array(messages)) => messages.insert(0, message),
96        Some(_) => return false,
97    }
98    true
99}
100
101// Preserved Responses instructions and Anthropic system prompts can be strings, missing, or null.
102fn prepend_text_field(body: &mut Map<String, Value>, key: &str, prompt: &str) -> bool {
103    match body.get_mut(key) {
104        None | Some(Value::Null) => {
105            body.insert(key.to_string(), Value::String(prompt.to_string()));
106        }
107        Some(Value::String(text)) if text.is_empty() => {
108            *text = prompt.to_string();
109        }
110        Some(Value::String(text)) => text.insert_str(0, &format!("{prompt}\n\n")),
111        Some(_) => return false,
112    }
113    true
114}
115
116// This helper retains existing Anthropic system blocks when it prepends a prompt.
117fn prepend_anthropic_system(body: &mut Map<String, Value>, prompt: &str) -> bool {
118    if let Some(Value::Array(blocks)) = body.get_mut("system") {
119        blocks.insert(0, json!({"type":"text","text":prompt}));
120        true
121    } else {
122        prepend_text_field(body, "system", prompt)
123    }
124}