GigaChat: honor resolved URLs and headers

This commit is contained in:
Alexander Davydov 2026-03-18 15:06:17 +03:00
parent af83a32c9e
commit 60e23072e5
2 changed files with 91 additions and 7 deletions

View File

@ -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,

View File

@ -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<string, string> | undefined {
if (!model.headers || typeof model.headers !== "object" || Array.isArray(model.headers)) {
return undefined;
}
return model.headers as Record<string, string>;
}
// ── Function name sanitization ──────────────────────────────────────────────
// GigaChat requires function names to be alphanumeric + underscore only.
@ -434,9 +443,10 @@ async function withRetry<T>(
// ── 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<string, string> = {
...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,
});