Skip to content
Merged
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
6 changes: 4 additions & 2 deletions open-sse/translator/helpers/openaiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ export function filterToOpenAIFormat(body) {
type: "function",
function: {
name: tool.name,
description: tool.description || "",
// Coerce: strict upstream validators (NVIDIA NIM, Codex) reject
// non-string descriptions. Ports decolua/9router#397.
description: String(tool.description ?? ""),
parameters: tool.input_schema || { type: "object", properties: {} },
},
};
Expand All @@ -190,7 +192,7 @@ export function filterToOpenAIFormat(body) {
type: "function",
function: {
name: fn.name,
description: fn.description || "",
description: String(fn.description ?? ""),
parameters: fn.parameters || { type: "object", properties: {} },
},
}));
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/translator-helper-tool-description-coercion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Regression: tool descriptions must always be strings for strict upstream
// validation (e.g., NVIDIA NIM, Codex). Ports upstream PR decolua/9router#397
// (Ibrahim Ryan), restricted to the gap that still exists in OmniRoute —
// the two non-string-tolerant paths in openaiHelper.filterToOpenAIFormat
// (Claude-style and Gemini-style tool normalization). The other locations
// upstream-patched (claude-to-openai.ts, openai-responses.ts) already coerce
// via String/toString in OmniRoute.
import test from "node:test";
import assert from "node:assert/strict";

const openaiHelper = await import(
"../../open-sse/translator/helpers/openaiHelper.ts"
);

test("filterToOpenAIFormat coerces non-string Claude-tool description to string", () => {
const result = openaiHelper.filterToOpenAIFormat({
messages: [{ role: "user", content: "hi" }],
tools: [
{
name: "claude-tool",
// Non-string truthy value — must NOT leak through to upstream.
description: { invalid: "object" } as unknown as string,
input_schema: { type: "object" },
},
],
});
assert.equal(typeof result.tools[0].function.description, "string");
});

test("filterToOpenAIFormat coerces null/undefined Claude-tool description to empty string", () => {
const result = openaiHelper.filterToOpenAIFormat({
messages: [{ role: "user", content: "hi" }],
tools: [
{ name: "t1", description: null as unknown as string, input_schema: {} },
{ name: "t2", description: undefined, input_schema: {} },
],
});
assert.equal(result.tools[0].function.description, "");
assert.equal(result.tools[1].function.description, "");
});

test("filterToOpenAIFormat coerces non-string Gemini functionDeclarations description", () => {
const result = openaiHelper.filterToOpenAIFormat({
messages: [{ role: "user", content: "hi" }],
tools: [
{
functionDeclarations: [
{
name: "gemini-tool",
description: 12345 as unknown as string,
parameters: { type: "object" },
},
],
},
],
});
assert.equal(typeof result.tools[0].function.description, "string");
});
Loading