From eae28f76234b85882f7fef8cfd25ecc12978ceb7 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Fri, 20 Mar 2026 11:59:16 +0300 Subject: [PATCH] GigaChat: rehydrate nested tool arguments --- src/agents/gigachat-stream.tool-calls.test.ts | 88 +++++++++++++ src/agents/gigachat-stream.ts | 117 ++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/src/agents/gigachat-stream.tool-calls.test.ts b/src/agents/gigachat-stream.tool-calls.test.ts index 832de40cb34..c05df0e6d68 100644 --- a/src/agents/gigachat-stream.tool-calls.test.ts +++ b/src/agents/gigachat-stream.tool-calls.test.ts @@ -228,6 +228,94 @@ describe("createGigachatStreamFn tool calling", () => { ]); }); + it("rehydrates nested JSON-string tool arguments before dispatching them", async () => { + const interactivePayload = { + blocks: [ + { + type: "buttons", + buttons: [{ label: "Approve", value: "approve" }], + }, + ], + }; + const argumentPayload = JSON.stringify({ + message: "Pick one", + interactive: JSON.stringify(interactivePayload), + }); + + request.mockResolvedValueOnce({ + status: 200, + data: createSseStream([ + `data: ${JSON.stringify({ + choices: [{ delta: { function_call: { name: "message", arguments: argumentPayload } } }], + })}`, + "data: [DONE]", + ]), + }); + + const streamFn = createGigachatStreamFn({ + baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", + authMode: "oauth", + }); + + const stream = await streamFn( + { api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never, + { + messages: [], + tools: [ + { + name: "message", + description: "Send a message", + parameters: { + type: "object", + properties: { + message: { type: "string" }, + interactive: { + type: "object", + properties: { + blocks: { + type: "array", + items: { + type: "object", + properties: { + type: { type: "string" }, + buttons: { + type: "array", + items: { + type: "object", + properties: { + label: { type: "string" }, + value: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + } as never, + { apiKey: "token" } as never, + ); + + const event = await stream.result(); + + expect(event.stopReason).toBe("toolUse"); + expect(event.content).toEqual([ + expect.objectContaining({ + type: "toolCall", + name: "message", + arguments: { + message: "Pick one", + interactive: interactivePayload, + }, + }), + ]); + }); + it("parses a final SSE frame even when the stream closes without a trailing newline", async () => { request.mockResolvedValueOnce({ status: 200, diff --git a/src/agents/gigachat-stream.ts b/src/agents/gigachat-stream.ts index c260fd8c4d4..d1f34bd33d0 100644 --- a/src/agents/gigachat-stream.ts +++ b/src/agents/gigachat-stream.ts @@ -309,6 +309,117 @@ function tryParseJsonObjectString(content: string): string | null { } } +function resolveSchemaType(schema: unknown): string | undefined { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) { + return undefined; + } + const type = (schema as Record).type; + if (typeof type === "string") { + return type; + } + if (Array.isArray(type)) { + return type.find((value): value is string => typeof value === "string" && value !== "null"); + } + return undefined; +} + +function tryParseJsonValue(content: string): unknown { + try { + return JSON.parse(content); + } catch { + return undefined; + } +} + +function rehydrateGigaChatArgumentValue(value: unknown, schema: unknown): unknown { + const schemaType = resolveSchemaType(schema); + const schemaObj = + schema && typeof schema === "object" && !Array.isArray(schema) + ? (schema as Record) + : undefined; + + if (schemaType === "object" || (!schemaType && schemaObj?.properties)) { + let objectValue = value; + if (typeof objectValue === "string") { + const parsed = tryParseJsonValue(objectValue); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return value; + } + objectValue = parsed; + } + if (!objectValue || typeof objectValue !== "object" || Array.isArray(objectValue)) { + return objectValue; + } + + const properties = + schemaObj?.properties && + typeof schemaObj.properties === "object" && + !Array.isArray(schemaObj.properties) + ? (schemaObj.properties as Record) + : undefined; + if (!properties) { + return objectValue; + } + + const nextObject = { ...(objectValue as Record) }; + for (const [key, propertySchema] of Object.entries(properties)) { + if (key in nextObject) { + nextObject[key] = rehydrateGigaChatArgumentValue(nextObject[key], propertySchema); + } + } + return nextObject; + } + + if (schemaType === "array") { + let arrayValue = value; + if (typeof arrayValue === "string") { + const parsed = tryParseJsonValue(arrayValue); + if (!Array.isArray(parsed)) { + return value; + } + arrayValue = parsed; + } + if (!Array.isArray(arrayValue)) { + return arrayValue; + } + + const itemSchema = schemaObj?.items; + if (!itemSchema) { + return arrayValue; + } + return arrayValue.map((item) => rehydrateGigaChatArgumentValue(item, itemSchema)); + } + + return value; +} + +function rehydrateGigaChatArguments( + args: Record, + schema: unknown, +): Record { + const schemaObj = + schema && typeof schema === "object" && !Array.isArray(schema) + ? (schema as Record) + : undefined; + const properties = + schemaObj?.properties && + typeof schemaObj.properties === "object" && + !Array.isArray(schemaObj.properties) + ? (schemaObj.properties as Record) + : undefined; + if (!properties) { + return args; + } + + const nextArgs = { ...args }; + for (const [key, propertySchema] of Object.entries(properties)) { + if (key in nextArgs) { + nextArgs[key] = rehydrateGigaChatArgumentValue(nextArgs[key], propertySchema); + } + } + return nextArgs; +} + /** * Coerce tool result content to a JSON object string (gpt2giga compatibility). * GigaChat expects tool results to be JSON objects. If the content is already @@ -608,6 +719,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { const functionsEnabled = disableFunctions !== "1" && disableFunctions !== "true"; const toolNameToGiga = new Map(); const gigaToToolName = new Map(); + const gigaToolSchemas = new Map(); // Build messages for GigaChat format const messages: Message[] = []; @@ -717,6 +829,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { if (sanitizedName !== tool.name) { log.debug(`GigaChat: sanitized function name "${tool.name}" → "${sanitizedName}"`); } + gigaToolSchemas.set(sanitizedName, tool.parameters); functions.push({ name: sanitizedName, @@ -902,6 +1015,10 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { const clientName = gigaToToolName.get(resolvedFunctionCall.name) ?? mapToolNameFromGigaChat(resolvedFunctionCall.name); + parsedArgs = rehydrateGigaChatArguments( + parsedArgs, + gigaToolSchemas.get(resolvedFunctionCall.name), + ); accumulatedToolCalls.push({ type: "toolCall", id: randomUUID(),