GigaChat: preserve parallel tool history

This commit is contained in:
Alexander Davydov 2026-03-16 15:08:10 +03:00
parent 95a75ffe01
commit 228cd48852
2 changed files with 113 additions and 17 deletions

View File

@ -150,4 +150,87 @@ describe("createGigachatStreamFn tool calling", () => {
);
expect(event.content).toEqual([{ type: "text", text: "done" }]);
});
it("preserves all historical tool calls from a single assistant turn", async () => {
request.mockResolvedValueOnce({
status: 200,
data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]),
});
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: [
{
role: "assistant",
content: [
{ type: "text", text: "Working on it" },
{
type: "toolCall",
id: "call_1",
name: "llm-task",
arguments: { prompt: "first" },
},
{
type: "toolCall",
id: "call_2",
name: "web_search",
arguments: { query: "second" },
},
],
},
],
tools: [
{
name: "llm-task",
description: "Run a task",
parameters: {
type: "object",
properties: {
prompt: { type: "string" },
},
},
},
{
name: "web_search",
description: "Search the web",
parameters: {
type: "object",
properties: {
query: { type: "string" },
},
},
},
],
} as never,
{ apiKey: "token" } as never,
);
const event = await stream.result();
expect(request).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
messages: [
expect.objectContaining({
role: "assistant",
content: "Working on it",
function_call: expect.objectContaining({ name: "llm_task" }),
}),
expect.objectContaining({
role: "assistant",
content: "",
function_call: expect.objectContaining({ name: "gpt2giga_user_search_web" }),
}),
],
}),
}),
);
expect(event.content).toEqual([{ type: "text", text: "done" }]);
});
});

View File

@ -39,6 +39,10 @@ export type GigachatStreamOptions = {
scope?: string;
};
function stripLeakedFunctionCallPrelude(text: string): string {
return text.replace(/^\s*assistant\s+function\s+call(?:recipient)?\{\s*/i, "");
}
// ── Function name sanitization ──────────────────────────────────────────────
// GigaChat requires function names to be alphanumeric + underscore only.
@ -485,26 +489,34 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.join("");
const toolCall = contentParts.find((c): c is ToolCall => c.type === "toolCall");
const toolCalls = contentParts.filter((c): c is ToolCall => c.type === "toolCall");
if (toolCall && toolCall.name && functionsEnabled) {
const gigaToolName = rememberToolNameMapping(
toolNameToGiga,
gigaToToolName,
toolCall.name,
);
messages.push({
role: "assistant",
content: text ? sanitizeContent(text) : "",
function_call: {
name: gigaToolName,
arguments: toolCall.arguments ?? {},
},
});
if (toolCalls.length > 0 && functionsEnabled) {
for (const [index, toolCall] of toolCalls.entries()) {
if (!toolCall.name) {
continue;
}
const gigaToolName = rememberToolNameMapping(
toolNameToGiga,
gigaToToolName,
toolCall.name,
);
messages.push({
role: "assistant",
content: index === 0 && text ? sanitizeContent(text) : "",
function_call: {
name: gigaToolName,
arguments: toolCall.arguments ?? {},
},
});
}
} else if (text) {
messages.push({ role: "assistant", content: sanitizeContent(text) });
} else if (toolCall && !functionsEnabled) {
messages.push({ role: "assistant", content: `[Called ${toolCall.name}]` });
} else if (toolCalls.length > 0 && !functionsEnabled) {
messages.push({
role: "assistant",
content: toolCalls.map((toolCall) => `[Called ${toolCall.name}]`).join(" "),
});
}
} else if (msg.role === "toolResult" && functionsEnabled) {
const toolName = msg.toolName ?? "unknown";
@ -729,6 +741,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
}
if (functionCallBuffer && functionCallBuffer.name) {
accumulatedContent = stripLeakedFunctionCallPrelude(accumulatedContent);
let parsedArgs: Record<string, unknown> = {};
try {
if (functionCallBuffer.arguments) {