Agents: reject GigaChat tool-name collisions

This commit is contained in:
Alexander Davydov 2026-03-16 15:58:17 +03:00
parent e814d0e1b3
commit 8fe44c4696
2 changed files with 47 additions and 6 deletions

View File

@ -1,5 +1,5 @@
import { Readable } from "node:stream";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const updateToken = vi.fn(async () => {});
const request = vi.fn();
@ -22,6 +22,10 @@ function createSseStream(lines: string[]): Readable {
}
describe("createGigachatStreamFn tool calling", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("round-trips sanitized tool names for streamed function calls", async () => {
request.mockResolvedValueOnce({
status: 200,
@ -156,7 +160,6 @@ describe("createGigachatStreamFn tool calling", () => {
status: 200,
data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]),
});
const streamFn = createGigachatStreamFn({
baseUrl: "https://gigachat.devices.sberbank.ru/api/v1",
authMode: "oauth",
@ -233,4 +236,41 @@ describe("createGigachatStreamFn tool calling", () => {
);
expect(event.content).toEqual([{ type: "text", text: "done" }]);
});
it("rejects tool-name sanitization collisions before sending the request", async () => {
const streamFn = createGigachatStreamFn({
baseUrl: "https://gigachat.devices.sberbank.ru/api/v1",
authMode: "oauth",
});
const stream = streamFn(
{ api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never,
{
messages: [],
tools: [
{
name: "llm-task",
description: "Run a task",
parameters: { type: "object", properties: {} },
},
{
name: "llm_task",
description: "Run another task",
parameters: { type: "object", properties: {} },
},
],
} as never,
{ apiKey: "token" } as never,
);
const event = await stream.result();
expect(event.stopReason).toBe("error");
expect(event.errorMessage).toBe(
'GigaChat tool name collision after sanitization: "llm_task" and "llm-task" both map to "llm_task"',
);
expect(event.content).toEqual([]);
expect(updateToken).not.toHaveBeenCalled();
expect(request).not.toHaveBeenCalled();
});
});

View File

@ -105,12 +105,13 @@ function rememberToolNameMapping(
originalName: string,
): string {
const gigaName = toGigaChatToolName(originalName);
const existingOriginalName = reverse.get(gigaName);
forward.set(originalName, gigaName);
if (!reverse.has(gigaName)) {
if (!existingOriginalName) {
reverse.set(gigaName, originalName);
} else if (reverse.get(gigaName) !== originalName) {
log.warn(
`GigaChat: tool name collision after sanitization: "${originalName}" and "${reverse.get(gigaName)}" both map to "${gigaName}"`,
} else if (existingOriginalName !== originalName) {
throw new Error(
`GigaChat tool name collision after sanitization: "${originalName}" and "${existingOriginalName}" both map to "${gigaName}"`,
);
}
return gigaName;