From 3f6d15f9aaa0cd67ca5e4cd98fd60431619de2b8 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 9 Nov 2026 17:02:27 -0800 Subject: [PATCH 1/2] fix(translation): reject duplicate Chat Completions tool names Signed-off-by: David Gardner --- .../src/codecs/openai_chat/buffered.rs | 7 ++++++ tests/test_translation.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs index a6af1d8eb..802a0dcf8 100644 --- a/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs +++ b/crates/switchyard-translation/src/codecs/openai_chat/buffered.rs @@ -1202,6 +1202,7 @@ fn media_source_text(source: &MediaSource) -> String { /// Encodes normalized tool definitions into OpenAI tool JSON. pub(crate) fn encode_openai_tools(tools: &[Tool]) -> Result { + let mut names = std::collections::HashSet::new(); Ok(Value::Array( tools .iter() @@ -1211,6 +1212,12 @@ pub(crate) fn encode_openai_tools(tools: &[Tool]) -> Result { "MCP and provider tool encoding is not implemented".into(), ) })?; + if !names.insert(tool.name.as_str()) { + return Err(TranslationError::InvalidValue { + path: "$.tools".to_string(), + message: format!("duplicate tool name: {:?}", tool.name), + }); + } let mut function = json!({ "name": tool.name, "description": tool.description.clone().unwrap_or_default(), diff --git a/tests/test_translation.py b/tests/test_translation.py index dd903bf9a..c85e9bc4e 100644 --- a/tests/test_translation.py +++ b/tests/test_translation.py @@ -252,6 +252,30 @@ def test_response_ir_restores_request_tool_extensions(method) -> None: ValueError, id="request", ), + pytest.param( + "translate_request", + ( + "openai_responses", + "openai_chat", + { + "tools": [ + { + "type": "namespace", + "name": "a__b", + "tools": [{"type": "function", "name": "c", "parameters": {}}], + }, + { + "type": "namespace", + "name": "a", + "tools": [{"type": "function", "name": "b__c", "parameters": {}}], + }, + ], + }, + ), + {}, + TranslationError, + id="namespace-tool-name-collision", + ), pytest.param( "encode_response", ("openai_chat", {}), From f8cf28fe15c786dad77ebd1b3126c6216776ddb1 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 10 Nov 2026 08:31:31 -0800 Subject: [PATCH 2/2] fix(translation): reject duplicate Anthropic tool names Signed-off-by: David Gardner --- .../src/codecs/anthropic/buffered.rs | 7 ++++++ tests/test_translation.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/crates/switchyard-translation/src/codecs/anthropic/buffered.rs b/crates/switchyard-translation/src/codecs/anthropic/buffered.rs index 5639f1765..695a9e544 100644 --- a/crates/switchyard-translation/src/codecs/anthropic/buffered.rs +++ b/crates/switchyard-translation/src/codecs/anthropic/buffered.rs @@ -1263,6 +1263,7 @@ fn ensure_anthropic_tool_input_object(arguments: Value) -> Value { // Encodes normalized tool definitions into Anthropic tool JSON. fn encode_anthropic_tools(tools: &[Tool]) -> Result { + let mut names = std::collections::HashSet::new(); Ok(Value::Array( tools .iter() @@ -1277,6 +1278,12 @@ fn encode_anthropic_tools(tools: &[Tool]) -> Result { "MCP and provider tool encoding is not implemented".into(), ) })?; + if !names.insert(tool.name.as_str()) { + return Err(TranslationError::InvalidValue { + path: "$.tools".to_string(), + message: format!("duplicate tool name: {:?}", tool.name), + }); + } let mut item = json!({ "name": tool.name, "description": tool.description.clone().unwrap_or_default(), diff --git a/tests/test_translation.py b/tests/test_translation.py index c85e9bc4e..fb1de2cbb 100644 --- a/tests/test_translation.py +++ b/tests/test_translation.py @@ -276,6 +276,30 @@ def test_response_ir_restores_request_tool_extensions(method) -> None: TranslationError, id="namespace-tool-name-collision", ), + pytest.param( + "translate_request", + ( + "openai_responses", + "anthropic_messages", + { + "tools": [ + { + "type": "namespace", + "name": "a__b", + "tools": [{"type": "function", "name": "c", "parameters": {}}], + }, + { + "type": "namespace", + "name": "a", + "tools": [{"type": "function", "name": "b__c", "parameters": {}}], + }, + ], + }, + ), + {}, + TranslationError, + id="namespace-tool-name-collision-anthropic", + ), pytest.param( "encode_response", ("openai_chat", {}),