Skip to content

Commit ca61338

Browse files
committed
fix(translator): preserve typed Anthropic server tools through full conversion
Addresses the follow-up Codex review on PR #3804. The normalizer already left typed Anthropic server tools (e.g. web_search_20250305) untouched, but the downstream tool mapper in ConvertOpenAIRequestToClaude only emitted type=="function" tools, silently dropping typed server tools from the final Claude request when sent alongside a bare custom tool. Pass typed server tools (type present, not "function") through verbatim into the Claude tools array. Add TestConvertOpenAIRequestToClaude_TypedServerToolPreserved to assert end-to-end survival through the full conversion, not just the normalizer.
1 parent 162200e commit ca61338

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

internal/translator/claude/openai/chat-completions/claude_openai_normalize_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,48 @@ func TestNormalizeAnthropicRequestBlocks_TypedToolNotWrapped(t *testing.T) {
186186
}
187187
}
188188

189+
func TestConvertOpenAIRequestToClaude_TypedServerToolPreserved(t *testing.T) {
190+
// End-to-end guard: a typed Anthropic server tool sent alongside a bare
191+
// custom tool must survive the FULL conversion, not just the normalizer.
192+
// The downstream tool mapper previously emitted only type=="function"
193+
// tools, silently dropping typed server tools like web_search_20250305.
194+
inputJSON := `{
195+
"model": "claude-sonnet-4-5",
196+
"messages": [{"role": "user", "content": "search the web"}],
197+
"tools": [
198+
{"type": "web_search_20250305", "name": "web_search", "max_uses": 5},
199+
{"name": "read_file", "description": "Read a file", "input_schema": {"type": "object"}}
200+
]
201+
}`
202+
203+
out := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
204+
outJSON := gjson.ParseBytes(out)
205+
tools := outJSON.Get("tools").Array()
206+
207+
if len(tools) != 2 {
208+
t.Fatalf("Expected 2 tools after full conversion, got %d: %s", len(tools), outJSON.Get("tools").Raw)
209+
}
210+
211+
typed := outJSON.Get(`tools.#(name=="web_search")`)
212+
if !typed.Exists() {
213+
t.Fatalf("Expected typed server tool web_search to survive conversion, got: %s", outJSON.Get("tools").Raw)
214+
}
215+
if got := typed.Get("type").String(); got != "web_search_20250305" {
216+
t.Fatalf("Expected typed tool type preserved as web_search_20250305, got %q (%s)", got, typed.Raw)
217+
}
218+
if got := typed.Get("max_uses").Int(); got != 5 {
219+
t.Fatalf("Expected typed tool fields preserved (max_uses=5), got %d", got)
220+
}
221+
222+
bare := outJSON.Get(`tools.#(name=="read_file")`)
223+
if !bare.Exists() {
224+
t.Fatalf("Expected bare custom tool read_file mapped to Claude tool, got: %s", outJSON.Get("tools").Raw)
225+
}
226+
if !bare.Get("input_schema").Exists() {
227+
t.Fatalf("Expected read_file mapped with input_schema, got: %s", bare.Raw)
228+
}
229+
}
230+
189231
func TestConvertOpenAIRequestToClaude_StandardOpenAIUnchanged(t *testing.T) {
190232
// A normal OpenAI payload must pass through normalization untouched.
191233
inputJSON := `{

internal/translator/claude/openai/chat-completions/claude_openai_request.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,12 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
298298

299299
out, _ = sjson.SetRawBytes(out, "tools.-1", anthropicTool)
300300
hasAnthropicTools = true
301+
} else if t := tool.Get("type").String(); t != "" {
302+
// Typed Anthropic server tools (e.g. {"type":"web_search_20250305",...})
303+
// are already in Claude's native shape. Pass them through unchanged
304+
// instead of dropping them, so they survive the full conversion.
305+
out, _ = sjson.SetRawBytes(out, "tools.-1", []byte(tool.Raw))
306+
hasAnthropicTools = true
301307
}
302308
return true
303309
})

0 commit comments

Comments
 (0)