switchyard_libsy/algorithms/util/
prompts.rs1use switchyard_protocol::Request;
7
8pub fn append_note(request: &mut Request, note: &str) {
10 switchyard_translation::append_note(&mut request.llm_request, note);
11}
12
13pub(crate) fn drop_exact_replay(request: &mut Request) {
16 request.llm_request.preservation.requests.clear();
17 request.llm_request.temporary_note_count = 0;
18}
19
20#[cfg(test)]
21mod tests {
22 use super::*;
23 use serde_json::json;
24 use switchyard_protocol::WireFormat;
25 use switchyard_translation::{TranslationEngine, TranslationPolicy};
26
27 #[test]
29 fn edits_preserve_native_prefixes_and_provider_fields() -> Result<(), Box<dyn std::error::Error>>
30 {
31 let engine = TranslationEngine::default();
32 let policy = TranslationPolicy::default();
33 let fixtures = [
34 (
35 WireFormat::OpenAiChat,
36 json!({
37 "model":"auto", "messages":[
38 {"role":"developer","content":[{"type":"text","text":"Keep this instruction.","vendor":"keep"}]},
39 {"role":"assistant","tool_calls":[{"id":"call_1","type":"function","function":{"name":"run","arguments":"{ \"cmd\": \"test\" }"}}]},
40 {"role":"tool","tool_call_id":"call_1","content":"exit 1","vendor":"keep"}
41 ], "metadata":{"caller":"keep"}, "temperature":0.2
42 }),
43 ),
44 (
45 WireFormat::OpenAiResponses,
46 json!({
47 "model":"auto", "instructions":"Keep this instruction.", "input":[
48 {"type":"message","role":"developer","content":[{"type":"input_text","text":"Keep this too."}]},
49 {"type":"function_call","call_id":"call_1","name":"run","arguments":"{ \"cmd\": \"test\" }"},
50 {"type":"function_call_output","call_id":"call_1","output":"exit 1"}
51 ], "metadata":{"caller":"keep"}, "store":false
52 }),
53 ),
54 (
55 WireFormat::AnthropicMessages,
56 json!({
57 "model":"auto", "max_tokens":32,
58 "system":[{"type":"text","text":"Keep this instruction.","cache_control":{"type":"ephemeral","ttl":"1h"}}],
59 "messages":[
60 {"role":"assistant","content":[{"type":"tool_use","id":"call_1","name":"run","input":{"cmd":"test"}}]},
61 {"role":"user","content":[{"type":"tool_result","tool_use_id":"call_1","content":"exit 1","is_error":true,"cache_control":{"type":"ephemeral"}}]}
62 ], "metadata":{"user_id":"keep"}
63 }),
64 ),
65 ];
66 for (format, original) in fixtures {
67 let mut request = Request {
68 llm_request: engine.decode_request(format, &original, &policy)?.request,
69 ..Default::default()
70 };
71 append_note(&mut request, "Reassess the failure.");
72 let encoded = engine
73 .encode_request(format, &request.llm_request, &policy)?
74 .body;
75 let mut expected = original.clone();
76 match format {
77 WireFormat::OpenAiChat => expected["messages"].as_array_mut().ok_or("messages")?.push(json!({"role":"user","content":[{"type":"text","text":"Reassess the failure."}]})),
78 WireFormat::OpenAiResponses => expected["input"].as_array_mut().ok_or("input")?.push(json!({"role":"user","content":[{"type":"input_text","text":"Reassess the failure."}]})),
79 WireFormat::AnthropicMessages => expected["messages"][1]["content"].as_array_mut().ok_or("content")?.push(json!({"type":"text","text":"Reassess the failure."})),
80 }
81 assert_eq!(encoded, expected, "{format}: only the note may change");
82 switchyard_translation::prepend_system_prompt(
83 &mut request.llm_request,
84 "Use the capable tier.",
85 );
86 let edited = engine
87 .encode_request(format, &request.llm_request, &policy)?
88 .body;
89 let key = if format == WireFormat::OpenAiResponses {
91 "input"
92 } else {
93 "messages"
94 };
95 if format == WireFormat::OpenAiChat {
96 assert_eq!(
97 edited[key].as_array().ok_or("messages")?[1..],
98 encoded[key].as_array().ok_or("messages")?[..]
99 );
100 } else {
101 assert_eq!(edited[key], encoded[key]);
102 }
103 assert_eq!(edited["metadata"], original["metadata"]);
104 }
105 Ok(())
106 }
107
108 #[test]
109 fn string_inputs_and_assistant_endings_keep_the_original_message()
110 -> Result<(), Box<dyn std::error::Error>> {
111 let engine = TranslationEngine::default();
112 let policy = TranslationPolicy::default();
113 for (format, body, key) in [
114 (
115 WireFormat::OpenAiChat,
116 json!({"model":"m","messages":[{"role":"assistant","content":"done"}]}),
117 "messages",
118 ),
119 (
120 WireFormat::AnthropicMessages,
121 json!({"model":"m","max_tokens":8,"messages":[{"role":"user","content":"question"},{"role":"assistant","content":"done"}]}),
122 "messages",
123 ),
124 (
125 WireFormat::OpenAiResponses,
126 json!({"model":"m","input":"question"}),
127 "input",
128 ),
129 ] {
130 let mut request = Request {
131 llm_request: engine.decode_request(format, &body, &policy)?.request,
132 ..Default::default()
133 };
134 append_note(&mut request, "note one");
135 append_note(&mut request, "note two");
136 let encoded = engine
137 .encode_request(format, &request.llm_request, &policy)?
138 .body;
139 let items = encoded[key].as_array().ok_or("items")?;
140 if body[key].is_array() {
141 let original = body[key].as_array().ok_or("original")?;
142 assert_eq!(&items[..original.len()], original);
143 } else {
144 assert_eq!(items[0], json!({"role":"user","content":"question"}));
145 }
146 }
147 Ok(())
148 }
149 #[test]
151 fn invalid_preservation_falls_back_without_losing_the_note()
152 -> Result<(), Box<dyn std::error::Error>> {
153 let engine = TranslationEngine::default();
154 let policy = TranslationPolicy::default();
155 let source = json!({"model":"m","messages":[{"role":"user","content":"original task"}]});
156 let mut request = Request {
157 llm_request: engine
158 .decode_request(WireFormat::OpenAiChat, &source, &policy)?
159 .request,
160 ..Default::default()
161 };
162 request
163 .llm_request
164 .preservation
165 .requests
166 .insert("custom".into(), json!({"messages":"unsupported"}));
167 request.llm_request.preservation.requests.insert(
168 WireFormat::OpenAiChat.into(),
169 json!({"model":"m","messages":"unsupported"}),
170 );
171 append_note(&mut request, "reassess the failure");
172 assert!(request.llm_request.preservation.requests.is_empty());
173 for target in [
174 WireFormat::OpenAiChat,
175 WireFormat::OpenAiResponses,
176 WireFormat::AnthropicMessages,
177 ] {
178 let body = engine
179 .encode_request(target, &request.llm_request, &policy)?
180 .body;
181 let decoded = engine.decode_request(target, &body, &policy)?.request;
182 let text: Vec<_> = decoded
183 .messages
184 .iter()
185 .filter_map(|message| message.text_content(""))
186 .collect();
187 assert_eq!(
188 text,
189 vec!["original task", "reassess the failure"],
190 "{target}"
191 );
192 }
193 Ok(())
194 }
195}