Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/switchyard-translation/src/codecs/responses/buffered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ impl FormatCodec for OpenAiResponsesCodec {
// does not accept. Keep only whether thinking is disabled.
let mut reasoning = if is_anthropic_request(request) {
let mut reasoning = Map::new();
if request
.reasoning
.raw
.as_ref()
.is_some_and(|thinking| thinking.get("budget_tokens").is_some())
{
push_lossy(
&mut diagnostics,
policy,
"Responses has no thinking token budget; dropped thinking.budget_tokens",
)?;
}
if request
.reasoning
.raw
Expand Down
68 changes: 68 additions & 0 deletions crates/switchyard-translation/tests/request_translation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,74 @@ fn anthropic_thinking_to_responses_uses_normalized_effort() -> TestResult {
Ok(())
}

// Verifies dropping an Anthropic thinking budget on the way to Responses is reported,
// and rejected when the caller asked for lossy conversions to fail.
#[test]
fn anthropic_thinking_budget_drop_to_responses_is_reported() -> TestResult {
let engine = TranslationEngine::default();
let body = json!({
"model": "route",
"max_tokens": 32000,
"messages": [{"role": "user", "content": "hi"}],
"thinking": {"type": "enabled", "budget_tokens": 10000}
});

let output = engine.translate_request(
WireFormat::AnthropicMessages,
WireFormat::OpenAiResponses,
&body,
&normalized_policy(),
)?;
assert_eq!(output.body.get("reasoning"), None);
assert_eq!(output.diagnostics.len(), 1);
assert_eq!(output.diagnostics[0].code, "lossy_conversion");
assert!(output.diagnostics[0].message.contains("budget_tokens"));

let policy = TranslationPolicy {
lossy_conversion_policy: LossyConversionPolicy::Reject,
..normalized_policy()
};
let result = engine.translate_request(
WireFormat::AnthropicMessages,
WireFormat::OpenAiResponses,
&body,
&policy,
);
assert!(result.is_err());
Ok(())
}

// Verifies Anthropic thinking controls that Responses can express translate without a
// lossy diagnostic.
#[test]
fn anthropic_thinking_without_budget_to_responses_is_not_reported() -> TestResult {
let engine = TranslationEngine::default();
let policy = TranslationPolicy {
lossy_conversion_policy: LossyConversionPolicy::Reject,
..normalized_policy()
};
for (thinking, output_config) in [
(json!({"type": "adaptive"}), json!({"effort": "low"})),
(json!({"type": "disabled"}), json!({})),
] {
let body = json!({
"model": "route",
"max_tokens": 4096,
"messages": [{"role": "user", "content": "hi"}],
"thinking": thinking,
"output_config": output_config
});
let output = engine.translate_request(
WireFormat::AnthropicMessages,
WireFormat::OpenAiResponses,
&body,
&policy,
)?;
assert!(output.diagnostics.is_empty());
}
Ok(())
}

#[test]
fn anthropic_reconstruction_preserves_fallback_credit_token() -> TestResult {
let engine = TranslationEngine::default();
Expand Down
Loading