GigaChat: preserve TLS overrides and tool results

This commit is contained in:
Alexander Davydov 2026-03-21 09:03:13 +03:00
parent c8e449f5c8
commit e909e4e926
7 changed files with 111 additions and 27 deletions

View File

@ -641,6 +641,73 @@ describe("createGigachatStreamFn tool calling", () => {
);
});
it("preserves historical tool results as plain text when functions are disabled", async () => {
vi.stubEnv("GIGACHAT_DISABLE_FUNCTIONS", "1");
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 = await streamFn(
{ api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never,
{
messages: [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "llm-task",
arguments: { prompt: "hi" },
},
],
},
{
role: "toolResult",
toolName: "llm-task",
content: '{"summary":"tool output"}',
},
],
tools: [
{
name: "llm-task",
description: "Run a task",
parameters: {
type: "object",
properties: {
prompt: { type: "string" },
},
},
},
],
} as never,
{ apiKey: "token" } as never,
);
await expect(stream.result()).resolves.toMatchObject({
content: [{ type: "text", text: "done" }],
});
const requestPayload = request.mock.calls[0]?.[0]?.data as {
messages?: Array<{ role: string; content?: string }>;
functions?: unknown;
};
expect(requestPayload.functions).toBeUndefined();
expect(requestPayload.messages).toEqual([
expect.objectContaining({ role: "assistant", content: "[Called llm-task]" }),
expect.objectContaining({
role: "user",
content: '[Tool Result: llm-task]\n{"summary":"tool output"}',
}),
]);
});
it("preserves all historical tool calls from a single assistant turn", async () => {
request.mockResolvedValueOnce({
status: 200,

View File

@ -296,6 +296,24 @@ function sanitizeContent(content: string | null | undefined): string {
);
}
function extractToolResultTextContent(content: unknown): string {
if (Array.isArray(content)) {
return content
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.join("\n");
}
if (typeof content === "string") {
return content;
}
return JSON.stringify(content ?? {});
}
function formatToolResultReplayText(toolName: string | undefined, content: unknown): string {
const replayContent = extractToolResultTextContent(content) || "ok";
return `[Tool Result: ${toolName?.trim() || "unknown"}]\n${replayContent}`;
}
function tryParseJsonObjectString(content: string): string | null {
const trimmed = content.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
@ -776,24 +794,27 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
content: toolCalls.map((toolCall) => `[Called ${toolCall.name}]`).join(" "),
});
}
} else if (msg.role === "toolResult" && functionsEnabled) {
} else if (msg.role === "toolResult") {
const toolName = msg.toolName ?? "unknown";
const msgContent = msg.content;
const resultContent = Array.isArray(msgContent)
? msgContent
.filter((c): c is TextContent => c.type === "text")
.map((c) => c.text)
.join("\n")
: typeof msgContent === "string"
? msgContent
: JSON.stringify(msgContent ?? {});
const coercedContent = ensureJsonObjectStr(resultContent || "ok", toolName);
const gigaToolName = rememberToolNameMapping(toolNameToGiga, gigaToToolName, toolName);
messages.push({
role: "function",
content: coercedContent,
name: gigaToolName,
});
if (functionsEnabled) {
const resultContent = extractToolResultTextContent(msg.content);
const coercedContent = ensureJsonObjectStr(resultContent || "ok", toolName);
const gigaToolName = rememberToolNameMapping(
toolNameToGiga,
gigaToToolName,
toolName,
);
messages.push({
role: "function",
content: coercedContent,
name: gigaToolName,
});
} else {
messages.push({
role: "user",
content: sanitizeContent(formatToolResultReplayText(toolName, msg.content)),
});
}
}
}

View File

@ -165,7 +165,6 @@ export async function applyAuthChoiceApiProviders(
{ secretInputMode: mode ?? requestedSecretInputMode },
{
authMode: "oauth",
insecureTls: "false",
scope: gigachatScope,
},
);
@ -248,7 +247,6 @@ export async function applyAuthChoiceApiProviders(
const basicMetadata: Record<string, string> = {
authMode: "basic",
insecureTls: "false",
...(gigachatBasicScope ? { scope: gigachatBasicScope } : {}),
};

View File

@ -342,10 +342,10 @@ describe("applyAuthChoice", () => {
key: "basic-user:basic-pass",
metadata: {
authMode: "basic",
insecureTls: "false",
scope: "GIGACHAT_API_PERS",
},
});
expect((await readAuthProfile("gigachat:default"))?.metadata).not.toHaveProperty("insecureTls");
expect((await readAuthProfile("gigachat:default"))?.keyRef).toBeUndefined();
});
@ -376,7 +376,6 @@ describe("applyAuthChoice", () => {
expect(await readAuthProfile("gigachat:default")).toMatchObject({
metadata: {
authMode: "basic",
insecureTls: "false",
scope: "GIGACHAT_API_B2B",
},
});
@ -447,9 +446,9 @@ describe("applyAuthChoice", () => {
metadata: {
authMode: "oauth",
scope: "GIGACHAT_API_PERS",
insecureTls: "false",
},
});
expect(profile?.metadata).not.toHaveProperty("insecureTls");
});
it("resets a custom Basic GigaChat base URL when switching to OAuth", async () => {
@ -497,7 +496,6 @@ describe("applyAuthChoice", () => {
metadata: {
authMode: "oauth",
scope: "GIGACHAT_API_PERS",
insecureTls: "false",
},
});
});

View File

@ -377,9 +377,12 @@ describe("onboard (non-interactive): provider auth", () => {
metadata: {
authMode: "oauth",
scope: "GIGACHAT_API_PERS",
insecureTls: "false",
},
});
const gigachatProfile = cfg.auth?.profiles?.["gigachat:default"] as
| { metadata?: Record<string, string> }
| undefined;
expect(gigachatProfile?.metadata).not.toHaveProperty("insecureTls");
});
});

View File

@ -168,7 +168,6 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
undefined,
{
authMode: "oauth",
insecureTls: "false",
scope: "GIGACHAT_API_PERS",
},
);
@ -282,7 +281,6 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => {
undefined,
{
authMode: "oauth",
insecureTls: "false",
scope: "GIGACHAT_API_PERS",
},
);

View File

@ -98,7 +98,6 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: {
!(await params.maybeSetResolvedApiKey(resolved, (value) =>
setGigachatApiKey(value, params.agentDir, params.apiKeyStorageOptions, {
authMode: "oauth",
insecureTls: "false",
scope: "GIGACHAT_API_PERS",
}),
))