mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704)
* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation
Some OpenAI-shape clients send a tool as a bare `{ function: {...} }`
object, omitting the spec-required `type: "function"` parent wrapper.
The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped
`tool.function` when `tool.type === "function"` was ALSO true, so a
bare-function tool fell through to `toolData = tool` (the wrapper
itself, with no `.name`), producing an empty `originalName` and
silently dropping the tool from the translated request — worse than
a 400, since the caller has no signal the tool never made it
upstream. Unwrap `tool.function` whenever present, independent of
the parent `type` field. Regression guard:
tests/unit/openai-to-claude-bare-tool.test.ts.
Co-authored-by: Samir Abis <me@samirabis.com>
Inspired-by: https://github.com/decolua/9router/pull/2473
* chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
---------
Co-authored-by: Samir Abis <me@samirabis.com>
This commit is contained in:
committed by
GitHub
parent
0840722826
commit
b45d10ceea
1
changelog.d/fixes/6704-unwrap-bare-function-tool.md
Normal file
1
changelog.d/fixes/6704-unwrap-bare-function-tool.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis)
|
||||
@@ -363,7 +363,15 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools
|
||||
.map((tool) => {
|
||||
const toolData = tool.type === "function" && tool.function ? tool.function : tool;
|
||||
// Function-shaped tools arrive in two flavors from real clients:
|
||||
// (a) openai-spec: { type: "function", function: { name, ... } }
|
||||
// (b) bare/loose: { function: { name, ... } } (no parent `type`)
|
||||
// Unwrap `tool.function` whenever it is present, regardless of the
|
||||
// parent `type` field — some OpenAI-shape clients omit the wrapper's
|
||||
// `type: "function"` entirely. Previously that bare shape fell
|
||||
// through to `toolData = tool` (the wrapper itself, with no `.name`),
|
||||
// producing an empty `originalName` and silently dropping the tool.
|
||||
const toolData = tool.function ?? tool;
|
||||
const originalName = typeof toolData.name === "string" ? toolData.name.trim() : "";
|
||||
|
||||
if (!originalName) {
|
||||
|
||||
65
tests/unit/openai-to-claude-bare-tool.test.ts
Normal file
65
tests/unit/openai-to-claude-bare-tool.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { openaiToClaudeRequest } = await import(
|
||||
"../../open-sse/translator/request/openai-to-claude.ts"
|
||||
);
|
||||
|
||||
// Regression: some OpenAI-shape clients send a tool as a BARE
|
||||
// `{ function: { name, description, parameters } }` object, omitting the
|
||||
// spec-required `type: "function"` parent wrapper. Before this fix, the
|
||||
// tools-mapping in openai-to-claude.ts only unwrapped `tool.function` when
|
||||
// `tool.type === "function"` was ALSO true, so a bare-function tool fell
|
||||
// through with `toolData === tool` (the wrapper itself, which has no
|
||||
// `.name`) — `originalName` came out empty and the tool was silently
|
||||
// dropped from the translated request (worse than a 400: the caller has no
|
||||
// idea the tool never made it upstream).
|
||||
|
||||
test("openaiToClaudeRequest: bare {function:{...}} tool (no parent type) is NOT dropped", () => {
|
||||
const request = {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [
|
||||
{
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
parameters: { type: "object", properties: { city: { type: "string" } } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const translated = openaiToClaudeRequest("claude-sonnet-4", request, false);
|
||||
|
||||
assert.ok(Array.isArray(translated.tools), "expected translated.tools to be an array");
|
||||
assert.equal(translated.tools.length, 1, "expected the bare-function tool to survive translation");
|
||||
|
||||
const tool = translated.tools[0];
|
||||
assert.match(tool.name, /get_weather$/, "expected the original tool name to be preserved (prefixed)");
|
||||
assert.equal(tool.description, "Get the current weather");
|
||||
assert.deepEqual(tool.input_schema, {
|
||||
type: "object",
|
||||
properties: { city: { type: "string" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("openaiToClaudeRequest: spec-shape {type:'function', function:{...}} tool still converts (no regression)", () => {
|
||||
const request = {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
parameters: { type: "object", properties: { city: { type: "string" } } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const translated = openaiToClaudeRequest("claude-sonnet-4", request, false);
|
||||
|
||||
assert.equal(translated.tools.length, 1);
|
||||
assert.match(translated.tools[0].name, /get_weather$/);
|
||||
});
|
||||
Reference in New Issue
Block a user