mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(stream): suppress unknown textual tool calls
This commit is contained in:
@@ -246,12 +246,34 @@ function containsMalformedTextualToolCall(text: unknown): boolean {
|
||||
return text.replace(/[\u200B-\u200D\uFEFF]/g, "").includes("[Tool call:");
|
||||
}
|
||||
|
||||
function extractAllowedToolNames(body: unknown): Set<string> | null {
|
||||
const record = asRecord(body);
|
||||
const tools = record.tools;
|
||||
if (!Array.isArray(tools)) return null;
|
||||
const names = new Set<string>();
|
||||
for (const tool of tools) {
|
||||
if (!tool || typeof tool !== "object" || Array.isArray(tool)) continue;
|
||||
const item = tool as JsonRecord;
|
||||
const directName = typeof item.name === "string" ? item.name.trim() : "";
|
||||
const fn =
|
||||
item.function && typeof item.function === "object" && !Array.isArray(item.function)
|
||||
? (item.function as JsonRecord)
|
||||
: null;
|
||||
const functionName = typeof fn?.name === "string" ? fn.name.trim() : "";
|
||||
const name = functionName || directName;
|
||||
if (name) names.add(name);
|
||||
}
|
||||
return names.size > 0 ? names : null;
|
||||
}
|
||||
|
||||
function collectPassthroughTextualToolCall(
|
||||
text: string,
|
||||
toolCalls: Map<string, ToolCall>
|
||||
toolCalls: Map<string, ToolCall>,
|
||||
allowedToolNames?: Set<string> | null
|
||||
): ToolCall | null {
|
||||
const parsed = parseTextualToolCallFromContent(text);
|
||||
if (!parsed) return null;
|
||||
if (allowedToolNames?.size && !allowedToolNames.has(parsed.name)) return null;
|
||||
const key = `textual:${toolCalls.size}`;
|
||||
const toolCall: ToolCall = {
|
||||
id: `call_${Date.now()}_${toolCalls.size}`,
|
||||
@@ -669,6 +691,7 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
/** Passthrough: accumulate tool_calls deltas for call log responseBody */
|
||||
const passthroughToolCalls = new Map<string, ToolCall>();
|
||||
let passthroughToolCallSeq = 0;
|
||||
const allowedToolNames = extractAllowedToolNames(body);
|
||||
let skipPassthroughEvent = false;
|
||||
|
||||
// State for translate mode (accumulatedContent for call log response body)
|
||||
@@ -1399,12 +1422,13 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (parsedCandidate?.kind === "complete") {
|
||||
const collectedToolCall = collectPassthroughTextualToolCall(
|
||||
bufferedCandidate,
|
||||
passthroughToolCalls
|
||||
passthroughToolCalls,
|
||||
allowedToolNames
|
||||
);
|
||||
if (collectedToolCall) {
|
||||
delta.tool_calls = [toStreamingToolCallDelta(collectedToolCall)];
|
||||
passthroughHasToolCalls = true;
|
||||
}
|
||||
passthroughHasToolCalls = true;
|
||||
textualToolCallConverted = true;
|
||||
passthroughBufferedTextualToolCallContent = "";
|
||||
delete delta.content;
|
||||
@@ -1760,14 +1784,18 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (
|
||||
collectPassthroughTextualToolCall(
|
||||
finalBufferedTextualToolCall,
|
||||
passthroughToolCalls
|
||||
passthroughToolCalls,
|
||||
allowedToolNames
|
||||
)
|
||||
) {
|
||||
passthroughHasToolCalls = true;
|
||||
}
|
||||
passthroughBufferedTextualToolCallContent = "";
|
||||
}
|
||||
if (content && collectPassthroughTextualToolCall(content, passthroughToolCalls)) {
|
||||
if (
|
||||
content &&
|
||||
collectPassthroughTextualToolCall(content, passthroughToolCalls, allowedToolNames)
|
||||
) {
|
||||
passthroughHasToolCalls = true;
|
||||
content = "";
|
||||
} else if (containsMalformedTextualToolCall(content)) {
|
||||
|
||||
@@ -232,6 +232,53 @@ test("createSSEStream passthrough converts split textual tool-call content at co
|
||||
assert.doesNotMatch(text, /\[Tool call: terminal\]/);
|
||||
});
|
||||
|
||||
test("createSSEStream passthrough suppresses textual tool calls for unknown tools", async () => {
|
||||
let onCompletePayload = null;
|
||||
const toolText = `[Tool call: search_files_ide]
|
||||
Arguments: {"path":"/opt/OmniRoute/src","target":"files"}`;
|
||||
|
||||
const text = await readTransformed(
|
||||
[
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_unknown_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: toolText } }],
|
||||
})}\n\n`,
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_unknown_textual_tool",
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`,
|
||||
],
|
||||
{
|
||||
mode: "passthrough",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
provider: "antigravity",
|
||||
model: "antigravity/gemini-3.5-flash-low",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "inspect files" }],
|
||||
tools: [
|
||||
{ type: "function", function: { name: "search_files", parameters: { type: "object" } } },
|
||||
],
|
||||
},
|
||||
onComplete(payload) {
|
||||
onCompletePayload = payload;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const choice = onCompletePayload.responseBody.choices[0];
|
||||
assert.equal(choice.finish_reason, "stop");
|
||||
assert.equal(choice.message.content, null);
|
||||
assert.equal(choice.message.tool_calls, undefined);
|
||||
assert.doesNotMatch(text, /search_files_ide/);
|
||||
assert.doesNotMatch(JSON.stringify(onCompletePayload.responseBody), /search_files_ide/);
|
||||
});
|
||||
|
||||
test("createSSEStream passthrough suppresses malformed textual tool-call content", async () => {
|
||||
let onCompletePayload = null;
|
||||
const malformedToolText = `(empty)[Tool call: terminal]\nArguments: {"command":"sqlite3 /opt/O\u200dmniRoute/data/o\u200dmniroute.`;
|
||||
|
||||
Reference in New Issue
Block a user