mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:30:45 +00:00
fix: Found one narrow regression risk in the new Ollama tool-call name (#74563)
Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -577,6 +577,48 @@ describe("convertToOllamaMessages", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves exact allowlisted tool-prefix names before Ollama replay", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: "call_1", name: "tool_a", arguments: { value: 1 } },
|
||||
{ type: "tool_use", id: "call_2", name: "tools_invoke_test", input: { value: 2 } },
|
||||
{ type: "toolCall", id: "call_3", name: "function-run", arguments: { value: 3 } },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = convertToOllamaMessages(messages, undefined, {
|
||||
availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]),
|
||||
});
|
||||
expect(result[0].tool_calls).toEqual([
|
||||
{ function: { name: "tool_a", arguments: { value: 1 } } },
|
||||
{ function: { name: "tools_invoke_test", arguments: { value: 2 } } },
|
||||
{ function: { name: "function-run", arguments: { value: 3 } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips underscore and dash provider prefixes only when the suffix is allowlisted", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: "call_1", name: "tools_exec", arguments: { command: "pwd" } },
|
||||
{ type: "tool_use", id: "call_2", name: "function-read", input: { path: "." } },
|
||||
{ type: "toolCall", id: "call_3", name: "tool_missing", arguments: {} },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = convertToOllamaMessages(messages, undefined, {
|
||||
availableToolNames: new Set(["exec", "read"]),
|
||||
});
|
||||
expect(result[0].tool_calls).toEqual([
|
||||
{ function: { name: "exec", arguments: { command: "pwd" } } },
|
||||
{ function: { name: "read", arguments: { path: "." } } },
|
||||
{ function: { name: "tool_missing", arguments: {} } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps non-prefixed Ollama replay tool names intact", () => {
|
||||
const messages = [
|
||||
{
|
||||
@@ -585,6 +627,7 @@ describe("convertToOllamaMessages", () => {
|
||||
{ type: "toolCall", id: "call_1", name: "functionshell", arguments: {} },
|
||||
{ type: "toolCall", id: "call_2", name: "tooling", arguments: {} },
|
||||
{ type: "toolCall", id: "call_3", name: "tools", arguments: {} },
|
||||
{ type: "toolCall", id: "call_4", name: "tool_a", arguments: {} },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -593,6 +636,7 @@ describe("convertToOllamaMessages", () => {
|
||||
{ function: { name: "functionshell", arguments: {} } },
|
||||
{ function: { name: "tooling", arguments: {} } },
|
||||
{ function: { name: "tools", arguments: {} } },
|
||||
{ function: { name: "tool_a", arguments: {} } },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -825,6 +869,39 @@ describe("buildAssistantMessage", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves exact allowlisted tool-prefix names in Ollama responses", () => {
|
||||
const response = {
|
||||
model: "qwen3:32b",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
message: {
|
||||
role: "assistant" as const,
|
||||
content: "",
|
||||
tool_calls: [
|
||||
{ function: { name: "tool_a", arguments: { value: 1 } } },
|
||||
{ function: { name: "tools_invoke_test", arguments: { value: 2 } } },
|
||||
{ function: { name: "function-run", arguments: { value: 3 } } },
|
||||
],
|
||||
},
|
||||
done: true,
|
||||
};
|
||||
const result = buildAssistantMessage(response, modelInfo, undefined, {
|
||||
availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]),
|
||||
});
|
||||
expect(result.content).toEqual([
|
||||
expect.objectContaining({ type: "toolCall", name: "tool_a", arguments: { value: 1 } }),
|
||||
expect.objectContaining({
|
||||
type: "toolCall",
|
||||
name: "tools_invoke_test",
|
||||
arguments: { value: 2 },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "toolCall",
|
||||
name: "function-run",
|
||||
arguments: { value: 3 },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps non-prefixed Ollama response tool names intact", () => {
|
||||
const response = {
|
||||
model: "qwen3:32b",
|
||||
@@ -836,6 +913,7 @@ describe("buildAssistantMessage", () => {
|
||||
{ function: { name: "functionshell", arguments: {} } },
|
||||
{ function: { name: "tooling", arguments: {} } },
|
||||
{ function: { name: "tools", arguments: {} } },
|
||||
{ function: { name: "tool_a", arguments: {} } },
|
||||
],
|
||||
},
|
||||
done: true,
|
||||
@@ -845,6 +923,7 @@ describe("buildAssistantMessage", () => {
|
||||
expect.objectContaining({ type: "toolCall", name: "functionshell", arguments: {} }),
|
||||
expect.objectContaining({ type: "toolCall", name: "tooling", arguments: {} }),
|
||||
expect.objectContaining({ type: "toolCall", name: "tools", arguments: {} }),
|
||||
expect.objectContaining({ type: "toolCall", name: "tool_a", arguments: {} }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -758,7 +758,14 @@ function normalizeOllamaToolSchema(schema: unknown, isRoot = false): Record<stri
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function extractToolCalls(content: unknown): OllamaToolCall[] {
|
||||
type OllamaToolCallNameOptions = {
|
||||
availableToolNames?: ReadonlySet<string>;
|
||||
};
|
||||
|
||||
function extractToolCalls(
|
||||
content: unknown,
|
||||
options: OllamaToolCallNameOptions = {},
|
||||
): OllamaToolCall[] {
|
||||
if (!Array.isArray(content)) {
|
||||
return [];
|
||||
}
|
||||
@@ -768,14 +775,14 @@ function extractToolCalls(content: unknown): OllamaToolCall[] {
|
||||
if (part.type === "toolCall") {
|
||||
result.push({
|
||||
function: {
|
||||
name: normalizeOllamaToolCallName(part.name),
|
||||
name: normalizeOllamaToolCallName(part.name, options),
|
||||
arguments: ensureArgsObject(part.arguments),
|
||||
},
|
||||
});
|
||||
} else if (part.type === "tool_use") {
|
||||
result.push({
|
||||
function: {
|
||||
name: normalizeOllamaToolCallName(part.name),
|
||||
name: normalizeOllamaToolCallName(part.name, options),
|
||||
arguments: ensureArgsObject(part.input),
|
||||
},
|
||||
});
|
||||
@@ -784,17 +791,51 @@ function extractToolCalls(content: unknown): OllamaToolCall[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeOllamaToolCallName(rawName: string): string {
|
||||
function buildOllamaToolNameSet(tools: Tool[] | undefined): ReadonlySet<string> | undefined {
|
||||
if (!tools || !Array.isArray(tools)) {
|
||||
return undefined;
|
||||
}
|
||||
const names = new Set<string>();
|
||||
for (const tool of tools) {
|
||||
if (typeof tool.name === "string" && tool.name.trim()) {
|
||||
names.add(tool.name.trim());
|
||||
}
|
||||
}
|
||||
return names.size > 0 ? names : undefined;
|
||||
}
|
||||
|
||||
function normalizeOllamaToolCallName(
|
||||
rawName: string,
|
||||
options: OllamaToolCallNameOptions = {},
|
||||
): string {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.replace(/^(?:functions?|tools?)[./_-]+/iu, "").trim();
|
||||
const availableToolNames = options.availableToolNames;
|
||||
if (availableToolNames?.has(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const strippedAnySeparator = trimmed.replace(/^(?:functions?|tools?)[./_-]+/iu, "").trim();
|
||||
if (
|
||||
availableToolNames &&
|
||||
strippedAnySeparator !== trimmed &&
|
||||
availableToolNames.has(strippedAnySeparator)
|
||||
) {
|
||||
return strippedAnySeparator;
|
||||
}
|
||||
if (availableToolNames) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed.replace(/^(?:functions?|tools?)[./]+/iu, "").trim();
|
||||
}
|
||||
|
||||
export function convertToOllamaMessages(
|
||||
messages: Array<{ role: string; content: unknown }>,
|
||||
system?: string,
|
||||
options: OllamaToolCallNameOptions = {},
|
||||
): OllamaChatMessage[] {
|
||||
const result: OllamaChatMessage[] = [];
|
||||
|
||||
@@ -816,7 +857,7 @@ export function convertToOllamaMessages(
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
const text = extractTextContent(msg.content);
|
||||
const toolCalls = extractToolCalls(msg.content);
|
||||
const toolCalls = extractToolCalls(msg.content, options);
|
||||
result.push({
|
||||
role: "assistant",
|
||||
content: text,
|
||||
@@ -867,6 +908,7 @@ export function buildAssistantMessage(
|
||||
response: OllamaChatResponse,
|
||||
modelInfo: StreamModelDescriptor,
|
||||
usageFallback?: OllamaUsageFallback,
|
||||
options: OllamaToolCallNameOptions = {},
|
||||
): AssistantMessage {
|
||||
const content: (TextContent | ThinkingContent | ToolCall)[] = [];
|
||||
const thinking = response.message.thinking ?? response.message.reasoning ?? "";
|
||||
@@ -884,7 +926,7 @@ export function buildAssistantMessage(
|
||||
content.push({
|
||||
type: "toolCall",
|
||||
id: `ollama_call_${randomUUID()}`,
|
||||
name: normalizeOllamaToolCallName(toolCall.function.name),
|
||||
name: normalizeOllamaToolCallName(toolCall.function.name, options),
|
||||
arguments: normalizeOllamaToolCallArguments(toolCall.function.arguments),
|
||||
});
|
||||
}
|
||||
@@ -976,9 +1018,14 @@ export function createOllamaStreamFn(
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const availableToolNames = buildOllamaToolNameSet(context.tools);
|
||||
const toolCallNameOptions: OllamaToolCallNameOptions = availableToolNames
|
||||
? { availableToolNames }
|
||||
: {};
|
||||
const ollamaMessages = convertToOllamaMessages(
|
||||
context.messages ?? [],
|
||||
context.systemPrompt,
|
||||
toolCallNameOptions,
|
||||
);
|
||||
const ollamaTools = extractOllamaTools(context.tools);
|
||||
|
||||
@@ -1214,7 +1261,12 @@ export function createOllamaStreamFn(
|
||||
input: estimateOllamaPromptTokens({ messages: ollamaMessages, tools: ollamaTools }),
|
||||
output: estimateOllamaCompletionTokens(finalResponse),
|
||||
};
|
||||
const assistantMessage = buildAssistantMessage(finalResponse, modelInfo, usageFallback);
|
||||
const assistantMessage = buildAssistantMessage(
|
||||
finalResponse,
|
||||
modelInfo,
|
||||
usageFallback,
|
||||
toolCallNameOptions,
|
||||
);
|
||||
closeThinkingBlock();
|
||||
closeTextBlock();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user