GigaChat: preserve parallel tool history
This commit is contained in:
parent
95a75ffe01
commit
228cd48852
@ -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" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user