Skip to content

Commit 55d2a22

Browse files
committed
feat(translation): prepare requests for routed targets
Signed-off-by: Alex Fournier <afournier@nvidia.com>
1 parent c7c07d5 commit 55d2a22

3 files changed

Lines changed: 87 additions & 3 deletions

File tree

crates/switchyard-translation/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ pub use llm::*;
3131
pub use policy::*;
3232
pub use stream::*;
3333
pub use util::{
34-
PRESERVATION_METADATA_KEY, normalize_anthropic_tool_use_ids, sanitize_anthropic_tool_use_id,
34+
PRESERVATION_METADATA_KEY, normalize_anthropic_tool_use_ids, prepare_request_for_target,
35+
sanitize_anthropic_tool_use_id,
3536
};

crates/switchyard-translation/src/util.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
use std::collections::BTreeMap;
77

88
use serde_json::{Map, Value, json};
9+
use switchyard_protocol::ModelId;
910

1011
use crate::diagnostic::TranslationDiagnostic;
1112
use crate::error::{Result, TranslationError};
1213
use crate::format::FormatId;
13-
use crate::llm::{ContentBlock, LlmRequest, Message, PreservationMetadata};
14+
use crate::llm::{ContentBlock, InstructionBlock, LlmRequest, Message, PreservationMetadata, Role};
1415
use crate::policy::{
1516
LossyConversionPolicy, PreservationPolicy, TranslationPolicy, UnknownFieldPolicy,
1617
};
@@ -271,6 +272,30 @@ pub fn exact_preserved_response(
271272
.flatten()
272273
}
273274

275+
/// Applies a selected target model and optionally prepends its system prompt.
276+
///
277+
/// Adding a prompt invalidates preserved provider bodies because they predate the mutation.
278+
/// Call this once per candidate using a request that has not already received a target prompt.
279+
pub fn prepare_request_for_target(
280+
request: &mut LlmRequest,
281+
target: &ModelId,
282+
prompt: Option<&str>,
283+
) {
284+
request.model = Some(target.to_string());
285+
if let Some(prompt) = prompt {
286+
request.instructions.insert(
287+
0,
288+
InstructionBlock {
289+
role: Role::System,
290+
content: vec![ContentBlock::Text {
291+
text: prompt.to_string(),
292+
}],
293+
},
294+
);
295+
request.preservation.requests.clear();
296+
}
297+
}
298+
274299
/// Embeds preservation metadata into a translated wire body when requested.
275300
pub fn embed_preservation(
276301
mut body: Value,

crates/switchyard-translation/tests/request_translation.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,69 @@ use pretty_assertions::assert_eq;
99
use serde_json::{Value, json};
1010
use switchyard_translation::{
1111
LossyConversionPolicy, TranslationEngine, TranslationPolicy, WireFormat,
12+
prepare_request_for_target,
1213
};
1314

1415
use common::{REASONING_MODEL, normalized_policy, shell_tool_call};
1516

16-
type TestResult = std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>;
17+
type TestResult<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
18+
19+
// A target prompt makes every preserved provider body stale.
20+
#[test]
21+
fn preparing_a_target_prompt_invalidates_exact_replay() -> TestResult {
22+
let engine = TranslationEngine::default();
23+
let policy = TranslationPolicy::default();
24+
let body = json!({
25+
"model": "route",
26+
"messages": [
27+
{"role": "system", "name": "caller", "content": "client prompt"},
28+
{"role": "user", "content": "hi"}
29+
]
30+
});
31+
let mut request = engine
32+
.decode_request(WireFormat::OpenAiChat, &body, &policy)?
33+
.request;
34+
35+
prepare_request_for_target(
36+
&mut request,
37+
&"selected/model".into(),
38+
Some("target prompt"),
39+
);
40+
41+
assert!(request.preservation.requests.is_empty());
42+
let encoded = engine
43+
.encode_request(WireFormat::OpenAiChat, &request, &policy)?
44+
.body;
45+
assert_eq!(encoded["model"], "selected/model");
46+
assert_eq!(encoded["messages"][0]["content"], "target prompt");
47+
assert_eq!(encoded["messages"][1]["content"], "client prompt");
48+
assert!(encoded["messages"][1].get("name").is_none());
49+
Ok(())
50+
}
51+
52+
// Stamping only the normalized target does not invalidate exact replay.
53+
#[test]
54+
fn preparing_without_a_prompt_preserves_exact_replay() -> TestResult {
55+
let engine = TranslationEngine::default();
56+
let policy = TranslationPolicy::default();
57+
let body = json!({
58+
"model": "route",
59+
"messages": [{"role": "user", "content": "hi"}],
60+
"provider_field": true
61+
});
62+
let mut request = engine
63+
.decode_request(WireFormat::OpenAiChat, &body, &policy)?
64+
.request;
65+
66+
prepare_request_for_target(&mut request, &"selected/model".into(), None);
67+
68+
assert_eq!(request.model.as_deref(), Some("selected/model"));
69+
assert_eq!(
70+
request.preservation.requests[&WireFormat::OpenAiChat.into()],
71+
body
72+
);
73+
Ok(())
74+
}
1775

1876
// Verifies Anthropic-only request fields are dropped or mapped for OpenAI Chat.
1977
#[test]

0 commit comments

Comments
 (0)