diff --git a/src/agents/gigachat-stream.tool-calls.test.ts b/src/agents/gigachat-stream.tool-calls.test.ts index e282d4995e8..873cc02e4e7 100644 --- a/src/agents/gigachat-stream.tool-calls.test.ts +++ b/src/agents/gigachat-stream.tool-calls.test.ts @@ -34,6 +34,7 @@ describe("createGigachatStreamFn tool calling", () => { beforeEach(() => { vi.clearAllMocks(); clientConfigs.length = 0; + vi.unstubAllEnvs(); }); it("round-trips sanitized tool names for streamed function calls", async () => { @@ -177,6 +178,75 @@ describe("createGigachatStreamFn tool calling", () => { expect(event.content).toEqual([{ type: "text", text: "final tail" }]); }); + it("prefers the resolved GigaChat baseUrl over the env override", async () => { + vi.stubEnv("GIGACHAT_BASE_URL", "https://env-host.example/api/v1"); + request.mockResolvedValueOnce({ + status: 200, + data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]), + }); + + const streamFn = createGigachatStreamFn({ + baseUrl: "https://resolved-host.example/api/v1", + authMode: "oauth", + }); + + const stream = await streamFn( + { api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never, + { messages: [], tools: [] } as never, + { apiKey: "token" } as never, + ); + + const event = await stream.result(); + + expect(event.content).toEqual([{ type: "text", text: "done" }]); + expect(clientConfigs).toHaveLength(1); + expect(clientConfigs[0]?.baseUrl).toBe("https://resolved-host.example/api/v1"); + }); + + it("forwards resolved model headers and caller headers on the custom transport", 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 = await streamFn( + { + api: "gigachat", + provider: "gigachat", + id: "GigaChat-2-Max", + headers: { + "X-Model-Header": "model-value", + }, + } as never, + { messages: [], tools: [] } as never, + { + apiKey: "token", + headers: { + "X-Caller-Header": "caller-value", + }, + } as never, + ); + + const event = await stream.result(); + + expect(event.content).toEqual([{ type: "text", text: "done" }]); + expect(request).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer test-token", + Accept: "text/event-stream", + "X-Model-Header": "model-value", + "X-Caller-Header": "caller-value", + }), + }), + ); + }); + it("sanitizes historical assistant/tool result names in the outbound request", async () => { request.mockResolvedValueOnce({ status: 200, diff --git a/src/agents/gigachat-stream.ts b/src/agents/gigachat-stream.ts index 504fe61a0a0..6714c599a7d 100644 --- a/src/agents/gigachat-stream.ts +++ b/src/agents/gigachat-stream.ts @@ -43,6 +43,15 @@ function stripLeakedFunctionCallPrelude(text: string): string { return text.replace(/^\s*assistant\s+function\s+call(?:\s*([A-Za-z0-9_.:/-]+))?\s*\{\s*/i, ""); } +function resolveGigachatModelHeaders(model: { + headers?: unknown; +}): Record | undefined { + if (!model.headers || typeof model.headers !== "object" || Array.isArray(model.headers)) { + return undefined; + } + return model.headers as Record; +} + // ── Function name sanitization ────────────────────────────────────────────── // GigaChat requires function names to be alphanumeric + underscore only. @@ -434,9 +443,10 @@ async function withRetry( // ── Stream function ───────────────────────────────────────────────────────── export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { + const configuredBaseUrl = opts.baseUrl.trim(); const envBaseUrl = process.env.GIGACHAT_BASE_URL?.trim(); const effectiveBaseUrl = - envBaseUrl || opts.baseUrl || "https://gigachat.devices.sberbank.ru/api/v1"; + configuredBaseUrl || envBaseUrl || "https://gigachat.devices.sberbank.ru/api/v1"; const envVerifySsl = process.env.GIGACHAT_VERIFY_SSL_CERTS?.trim().toLowerCase(); const insecureTls = opts.insecureTls ?? (envVerifySsl === "false" || envVerifySsl === "0"); @@ -650,17 +660,21 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn { const requestId = randomUUID(); log.debug(`GigaChat request ${requestId}: starting`); + const headers: Record = { + ...resolveGigachatModelHeaders(model), + ...options?.headers, + Authorization: `Bearer ${accessToken}`, + Accept: "text/event-stream", + "Cache-Control": "no-store", + "X-Request-ID": requestId, + }; + const response = await axiosClient.request({ method: "POST", url: "/chat/completions", data: { ...chatRequest, stream: true }, responseType: "stream", - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: "text/event-stream", - "Cache-Control": "no-store", - "X-Request-ID": requestId, - }, + headers, signal: options?.signal, });