mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
fix(sse): skip empty-name tool calls in Responses API translator
Prevents infinite retry loops when models generate tool calls with empty function names. The normalizeToolName function converted these to "placeholder_tool" which does not exist in any client's tool registry, causing repeated error-retry cycles.
This commit is contained in:
@@ -120,6 +120,12 @@ export function openaiResponsesToOpenAIRequest(
|
||||
}
|
||||
|
||||
if (itemType === "function_call") {
|
||||
// Skip tool calls with empty names to avoid infinite placeholder_tool loops
|
||||
const fnName = toString(item.name);
|
||||
if (!fnName || !fnName.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start or append assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
@@ -136,7 +142,7 @@ export function openaiResponsesToOpenAIRequest(
|
||||
id: toString(item.call_id),
|
||||
type: "function",
|
||||
function: {
|
||||
name: toString(item.name),
|
||||
name: fnName,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
});
|
||||
@@ -319,10 +325,15 @@ export function openaiToOpenAIResponsesRequest(
|
||||
for (const toolCallValue of msg.tool_calls) {
|
||||
const toolCall = toRecord(toolCallValue);
|
||||
const fn = toRecord(toolCall.function);
|
||||
// Skip tool calls with empty names to avoid infinite placeholder_tool loops
|
||||
const fnName = toString(fn.name).trim();
|
||||
if (!fnName) {
|
||||
continue;
|
||||
}
|
||||
input.push({
|
||||
type: "function_call",
|
||||
call_id: toString(toolCall.id),
|
||||
name: toString(fn.name),
|
||||
name: fnName,
|
||||
arguments: toString(fn.arguments, "{}"),
|
||||
});
|
||||
}
|
||||
|
||||
180
tests/unit/empty-tool-name-loop.test.mjs
Normal file
180
tests/unit/empty-tool-name-loop.test.mjs
Normal file
@@ -0,0 +1,180 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { openaiResponsesToOpenAIRequest, openaiToOpenAIResponsesRequest } =
|
||||
await import("../../open-sse/translator/request/openai-responses.ts");
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Responses API -> Chat Completions direction
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("openaiResponsesToOpenAIRequest: skips function_call items with empty name", () => {
|
||||
const body = {
|
||||
model: "gpt-4",
|
||||
input: [
|
||||
{ type: "message", role: "user", content: "hello" },
|
||||
{ type: "function_call", call_id: "call_1", name: "", arguments: "{}" },
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiResponsesToOpenAIRequest("gpt-4", body, true, {});
|
||||
const messages = result.messages;
|
||||
|
||||
// Should have only the user message, no assistant message with empty tool call
|
||||
const assistantMsgs = messages.filter((m) => m.role === "assistant");
|
||||
assert.equal(assistantMsgs.length, 0, "empty-name function_call should be skipped entirely");
|
||||
});
|
||||
|
||||
test("openaiResponsesToOpenAIRequest: skips function_call items with whitespace-only name", () => {
|
||||
const body = {
|
||||
model: "gpt-4",
|
||||
input: [
|
||||
{ type: "message", role: "user", content: "hello" },
|
||||
{ type: "function_call", call_id: "call_1", name: " ", arguments: "{}" },
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiResponsesToOpenAIRequest("gpt-4", body, true, {});
|
||||
const messages = result.messages;
|
||||
|
||||
const assistantMsgs = messages.filter((m) => m.role === "assistant");
|
||||
assert.equal(assistantMsgs.length, 0, "whitespace-only name function_call should be skipped");
|
||||
});
|
||||
|
||||
test("openaiResponsesToOpenAIRequest: keeps function_call items with valid name", () => {
|
||||
const body = {
|
||||
model: "gpt-4",
|
||||
input: [
|
||||
{ type: "message", role: "user", content: "hello" },
|
||||
{
|
||||
type: "function_call",
|
||||
call_id: "call_1",
|
||||
name: "get_weather",
|
||||
arguments: '{"city":"NYC"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiResponsesToOpenAIRequest("gpt-4", body, true, {});
|
||||
const messages = result.messages;
|
||||
|
||||
const assistantMsgs = messages.filter((m) => m.role === "assistant");
|
||||
assert.equal(assistantMsgs.length, 1, "valid function_call should produce assistant message");
|
||||
assert.equal(assistantMsgs[0].tool_calls.length, 1);
|
||||
assert.equal(assistantMsgs[0].tool_calls[0].function.name, "get_weather");
|
||||
});
|
||||
|
||||
test("openaiResponsesToOpenAIRequest: mixed valid and empty names keeps only valid", () => {
|
||||
const body = {
|
||||
model: "gpt-4",
|
||||
input: [
|
||||
{ type: "message", role: "user", content: "hello" },
|
||||
{ type: "function_call", call_id: "call_1", name: "", arguments: "{}" },
|
||||
{
|
||||
type: "function_call",
|
||||
call_id: "call_2",
|
||||
name: "get_weather",
|
||||
arguments: '{"city":"NYC"}',
|
||||
},
|
||||
{ type: "function_call", call_id: "call_3", name: " ", arguments: "{}" },
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiResponsesToOpenAIRequest("gpt-4", body, true, {});
|
||||
const messages = result.messages;
|
||||
|
||||
const assistantMsgs = messages.filter((m) => m.role === "assistant");
|
||||
assert.equal(assistantMsgs.length, 1);
|
||||
assert.equal(assistantMsgs[0].tool_calls.length, 1, "only valid tool call should remain");
|
||||
assert.equal(assistantMsgs[0].tool_calls[0].function.name, "get_weather");
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Chat Completions -> Responses API direction
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("openaiToOpenAIResponsesRequest: skips tool_calls with empty function name", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [{ id: "call_1", type: "function", function: { name: "", arguments: "{}" } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, {});
|
||||
const fnCalls = result.input.filter((i) => i.type === "function_call");
|
||||
assert.equal(fnCalls.length, 0, "empty-name tool_call should be skipped");
|
||||
});
|
||||
|
||||
test("openaiToOpenAIResponsesRequest: skips tool_calls with whitespace-only function name", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{ id: "call_1", type: "function", function: { name: " ", arguments: "{}" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, {});
|
||||
const fnCalls = result.input.filter((i) => i.type === "function_call");
|
||||
assert.equal(fnCalls.length, 0, "whitespace-only name tool_call should be skipped");
|
||||
});
|
||||
|
||||
test("openaiToOpenAIResponsesRequest: keeps tool_calls with valid function name", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: '{"city":"NYC"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, {});
|
||||
const fnCalls = result.input.filter((i) => i.type === "function_call");
|
||||
assert.equal(fnCalls.length, 1);
|
||||
assert.equal(fnCalls[0].name, "get_weather");
|
||||
});
|
||||
|
||||
test("openaiToOpenAIResponsesRequest: mixed valid and empty names keeps only valid", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{ id: "call_1", type: "function", function: { name: "", arguments: "{}" } },
|
||||
{
|
||||
id: "call_2",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: '{"city":"NYC"}' },
|
||||
},
|
||||
{ id: "call_3", type: "function", function: { name: " \t ", arguments: "{}" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, {});
|
||||
const fnCalls = result.input.filter((i) => i.type === "function_call");
|
||||
assert.equal(fnCalls.length, 1, "only valid tool call should remain");
|
||||
assert.equal(fnCalls[0].name, "get_weather");
|
||||
});
|
||||
Reference in New Issue
Block a user