From 5dd96c970a1c928989ce1029780cef1befef9627 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Fri, 20 Mar 2026 14:16:17 +0300 Subject: [PATCH] GigaChat: honor config-backed run auth --- .../pi-embedded-runner/run/attempt.test.ts | 65 +++++++++++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 52 +++++++++++++-- 2 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 99b81736b10..781dda22529 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -9,6 +9,7 @@ import { composeSystemPromptWithHookContext, isOllamaCompatProvider, prependSystemPromptAddition, + resolveGigachatApiKeyForRun, resolveGigachatAuthProfileMetadata, resolveAttemptFsWorkspaceOnly, resolveOllamaCompatNumCtxEnabled, @@ -252,6 +253,70 @@ describe("resolveGigachatAuthMode", () => { }); }); +describe("resolveGigachatApiKeyForRun", () => { + it("falls back to config-backed GigaChat API keys when authStorage has no key", async () => { + const resolved = await resolveGigachatApiKeyForRun({ + model: { + provider: "gigachat", + api: "openai-completions", + id: "GigaChat-2-Max", + input: ["text"], + } as never, + config: { + models: { + providers: { + gigachat: { + baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", + api: "openai-completions", + apiKey: "user:password", + models: [], + }, + }, + }, + }, + authStorage: { + getApiKey: vi.fn(async () => undefined), + }, + }); + + expect(resolved).toEqual({ + apiKey: "user:password", + authProfileId: undefined, + }); + }); + + it("keeps runtime authStorage keys over config-backed GigaChat API keys", async () => { + const resolved = await resolveGigachatApiKeyForRun({ + model: { + provider: "gigachat", + api: "openai-completions", + id: "GigaChat-2-Max", + input: ["text"], + } as never, + config: { + models: { + providers: { + gigachat: { + baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", + api: "openai-completions", + apiKey: "config-user:config-pass", + models: [], + }, + }, + }, + }, + authStorage: { + getApiKey: vi.fn(async () => "runtime-key"), + }, + }); + + expect(resolved).toEqual({ + apiKey: "runtime-key", + authProfileId: undefined, + }); + }); +}); + describe("composeSystemPromptWithHookContext", () => { it("returns undefined when no hook system context is provided", () => { expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined(); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 1be3df5903f..b5a9a5a7576 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -60,7 +60,7 @@ import { } from "../../gigachat-auth.js"; import { createGigachatStreamFn } from "../../gigachat-stream.js"; import { resolveImageSanitizationLimits } from "../../image-sanitization.js"; -import { resolveModelAuthMode } from "../../model-auth.js"; +import { getApiKeyForModel, resolveModelAuthMode } from "../../model-auth.js"; import { resolveToolCallArgumentsEncoding } from "../../model-compat.js"; import { normalizeProviderId, resolveDefaultModelForAgent } from "../../model-selection.js"; import { supportsModelTools } from "../../model-tool-support.js"; @@ -230,6 +230,40 @@ function createYieldAbortedResponse(model: { api?: string; provider?: string; id } export { resolveGigachatAuthProfileMetadata } from "../../gigachat-auth.js"; +export async function resolveGigachatApiKeyForRun(params: { + model: EmbeddedRunAttemptParams["model"]; + config?: OpenClawConfig; + authProfileId?: string; + agentDir?: string; + authStorage: Pick; +}): Promise<{ apiKey?: string; authProfileId?: string }> { + const runtimeApiKey = await params.authStorage.getApiKey(params.model.provider); + let resolvedApiKey = runtimeApiKey ?? undefined; + let resolvedAuthProfileId = params.authProfileId?.trim() || undefined; + + if (!resolvedApiKey || !resolvedAuthProfileId) { + try { + const resolvedAuth = await getApiKeyForModel({ + model: params.model, + cfg: params.config, + profileId: params.authProfileId, + agentDir: params.agentDir, + }); + resolvedApiKey ??= resolvedAuth.apiKey ?? undefined; + resolvedAuthProfileId ||= resolvedAuth.profileId?.trim() || undefined; + } catch (error) { + if (!resolvedApiKey) { + throw error; + } + } + } + + return { + apiKey: resolvedApiKey, + authProfileId: resolvedAuthProfileId, + }; +} + // Queue a hidden steering message so pi-agent-core skips any remaining tool calls. function queueSessionsYieldInterruptMessage(activeSession: { agent: { steer: (message: AgentMessage) => void }; @@ -1976,24 +2010,30 @@ export async function runEmbeddedAttempt( (typeof params.model.baseUrl === "string" ? params.model.baseUrl : undefined) ?? process.env.GIGACHAT_BASE_URL?.trim() ?? "https://gigachat.devices.sberbank.ru/api/v1"; + const resolvedGigachatAuth = await resolveGigachatApiKeyForRun({ + model: params.model, + config: params.config, + authProfileId: params.authProfileId, + agentDir, + authStorage: params.authStorage, + }); // Read GigaChat-specific config from auth profile credential metadata. const gigachatStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const gigachatMeta = resolveGigachatAuthProfileMetadata( gigachatStore, - params.authProfileId, + resolvedGigachatAuth.authProfileId, { - allowDefaultProfileFallback: Boolean(params.authProfileId?.trim()), + allowDefaultProfileFallback: Boolean(resolvedGigachatAuth.authProfileId), }, ); - const gigachatApiKey = await params.authStorage.getApiKey(params.provider); const gigachatStreamFn = createGigachatStreamFn({ baseUrl, authMode: resolveGigachatAuthMode({ metadata: gigachatMeta, - apiKey: gigachatApiKey ?? undefined, - authProfileId: params.authProfileId, + apiKey: resolvedGigachatAuth.apiKey, + authProfileId: resolvedGigachatAuth.authProfileId, }), insecureTls: gigachatMeta?.insecureTls === "true", scope: gigachatMeta?.scope,