From c46ec2ebf429d77332e6de3a089350e1089ef0f1 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Thu, 19 Mar 2026 23:51:21 +0300 Subject: [PATCH] GigaChat: reset Basic base URLs on OAuth reauth --- .../auth-choice.apply.api-providers.ts | 24 +++++++- src/commands/auth-choice.test.ts | 51 ++++++++++++++++ .../auth-choice.api-key-providers.test.ts | 61 +++++++++++++++++++ .../local/auth-choice.api-key-providers.ts | 13 ++++ 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 6d0b07e2170..15694787aa0 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -1,3 +1,4 @@ +import { loadAuthProfileStoreForSecretsRuntime } from "../agents/auth-profiles.js"; import { resolveGigachatAuthMode } from "../agents/gigachat-auth.js"; import { resolveManifestProviderApiKeyChoice } from "../plugins/provider-auth-choices.js"; import { ensureApiKeyFromOptionEnvOrPrompt } from "./auth-choice.apply-helpers.js"; @@ -15,6 +16,7 @@ import { GIGACHAT_DEFAULT_MODEL_REF, setGigachatApiKey, } from "./onboard-auth.js"; +import { GIGACHAT_BASE_URL } from "./onboard-auth.models.js"; import type { AuthChoice } from "./onboard-types.js"; const CORE_API_KEY_TOKEN_PROVIDER_AUTH_CHOICES: Partial> = { @@ -22,6 +24,15 @@ const CORE_API_KEY_TOKEN_PROVIDER_AUTH_CHOICES: Partial + applyGigachatConfig( + config, + resetGigachatBaseUrl ? { baseUrl: GIGACHAT_BASE_URL } : undefined, + ), + applyProviderConfig: (config) => + applyGigachatProviderConfig( + config, + resetGigachatBaseUrl ? { baseUrl: GIGACHAT_BASE_URL } : undefined, + ), noteDefault: GIGACHAT_DEFAULT_MODEL_REF, }); return { config: nextConfig, agentModelOverride }; diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 2ccf06bdffc..3cceb592319 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -37,6 +37,7 @@ import { registerProviderPlugins } from "../test-utils/plugin-registration.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; import { GOOGLE_GEMINI_DEFAULT_MODEL } from "./google-gemini-model-default.js"; +import { GIGACHAT_BASE_URL } from "./onboard-auth.models.js"; import type { AuthChoice } from "./onboard-types.js"; import { authProfilePathForAgent, @@ -382,6 +383,56 @@ describe("applyAuthChoice", () => { await expect(readAuthProfile("gigachat:default")).rejects.toThrow(); }); + it("resets a custom Basic GigaChat base URL when switching to OAuth", async () => { + await setupTempState(); + + delete process.env.GIGACHAT_CREDENTIALS; + delete process.env.GIGACHAT_USER; + delete process.env.GIGACHAT_PASSWORD; + delete process.env.GIGACHAT_BASE_URL; + + const basicText = vi + .fn() + .mockResolvedValueOnce("https://preview-basic.gigachat.example/api/v1") + .mockResolvedValueOnce("basic-user") + .mockResolvedValueOnce("basic-pass"); + const basicHarness = createApiKeyPromptHarness({ text: basicText }); + + const basicResult = await applyAuthChoice({ + authChoice: "gigachat-basic", + config: {}, + prompter: basicHarness.prompter, + runtime: basicHarness.runtime, + setDefaultModel: true, + }); + + expect(basicResult.config.models?.providers?.gigachat?.baseUrl).toBe( + "https://preview-basic.gigachat.example/api/v1", + ); + + process.env.GIGACHAT_CREDENTIALS = "gigachat-oauth-credentials=="; // pragma: allowlist secret + const oauthHarness = createApiKeyPromptHarness(); + + const oauthResult = await applyAuthChoice({ + authChoice: "gigachat-personal", + config: basicResult.config, + prompter: oauthHarness.prompter, + runtime: oauthHarness.runtime, + setDefaultModel: true, + }); + + expect(oauthResult.config.models?.providers?.gigachat?.baseUrl).toBe(GIGACHAT_BASE_URL); + expect(await readAuthProfile("gigachat:default")).toMatchObject({ + type: "api_key", + provider: "gigachat", + metadata: { + authMode: "oauth", + scope: "GIGACHAT_API_PERS", + insecureTls: "false", + }, + }); + }); + it("prompts and writes provider API key for common providers", async () => { const scenarios: Array<{ authChoice: diff --git a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts index 82d64eb5b27..8bf01ed1693 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.test.ts @@ -1,8 +1,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ApiKeyCredential, AuthProfileStore } from "../../../agents/auth-profiles.js"; import type { OpenClawConfig } from "../../../config/config.js"; import type { RuntimeEnv } from "../../../runtime.js"; +import { GIGACHAT_BASE_URL } from "../../onboard-auth.models.js"; import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js"; +const loadAuthProfileStoreForSecretsRuntime = vi.hoisted(() => + vi.fn<() => AuthProfileStore>(() => ({ version: 1, profiles: {} })), +); +vi.mock("../../../agents/auth-profiles.js", () => ({ + loadAuthProfileStoreForSecretsRuntime, +})); + const applyAuthProfileConfig = vi.hoisted(() => vi.fn((cfg: OpenClawConfig) => cfg)); vi.mock("../../../plugins/provider-auth-helpers.js", () => ({ applyAuthProfileConfig, @@ -28,6 +37,7 @@ vi.mock("../../onboard-auth.config-litellm.js", () => ({ describe("applySimpleNonInteractiveApiKeyChoice", () => { beforeEach(() => { vi.clearAllMocks(); + loadAuthProfileStoreForSecretsRuntime.mockReturnValue({ version: 1, profiles: {} }); applyAuthProfileConfig.mockImplementation((cfg: OpenClawConfig) => cfg); applyGigachatConfig.mockImplementation((cfg: OpenClawConfig) => cfg); applyLitellmConfig.mockImplementation((cfg: OpenClawConfig) => cfg); @@ -151,4 +161,55 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { ); expect(runtime.exit).toHaveBeenCalledWith(1); }); + + it("resets the GigaChat provider base URL when replacing a Basic profile with OAuth", async () => { + const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; + const basicProfile: ApiKeyCredential = { + type: "api_key", + provider: "gigachat", + key: "basic-user:basic-pass", + metadata: { + authMode: "basic", + }, + }; + loadAuthProfileStoreForSecretsRuntime.mockReturnValue({ + version: 1, + profiles: { + "gigachat:default": basicProfile, + }, + }); + const resolveApiKey = vi.fn(async () => ({ + key: "gigachat-oauth-credentials", + source: "flag" as const, + })); + const maybeSetResolvedApiKey = vi.fn(async (resolved, setter) => { + await setter(resolved.key); + return true; + }); + + await applySimpleNonInteractiveApiKeyChoice({ + authChoice: "gigachat-oauth", + nextConfig, + baseConfig: { + models: { + providers: { + gigachat: { + baseUrl: "https://preview-basic.gigachat.example/api/v1", + api: "openai-completions", + models: [], + }, + }, + }, + } as OpenClawConfig, + opts: { token: "gigachat-oauth-credentials" } as never, + runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() } as never, + apiKeyStorageOptions: undefined, + resolveApiKey, + maybeSetResolvedApiKey, + }); + + expect(applyGigachatConfig).toHaveBeenCalledWith(expect.any(Object), { + baseUrl: GIGACHAT_BASE_URL, + }); + }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts index e8a86cebbcf..577e9589915 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.api-key-providers.ts @@ -1,3 +1,4 @@ +import { loadAuthProfileStoreForSecretsRuntime } from "../../../agents/auth-profiles.js"; import { resolveGigachatAuthMode } from "../../../agents/gigachat-auth.js"; import type { OpenClawConfig } from "../../../config/config.js"; import type { SecretInput } from "../../../config/types.secrets.js"; @@ -6,6 +7,7 @@ import { setGigachatApiKey, setLitellmApiKey } from "../../../plugins/provider-a import type { RuntimeEnv } from "../../../runtime.js"; import { applyGigachatConfig } from "../../onboard-auth.config-core.js"; import { applyLitellmConfig } from "../../onboard-auth.config-litellm.js"; +import { GIGACHAT_BASE_URL } from "../../onboard-auth.models.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; type ApiKeyStorageOptions = { @@ -17,6 +19,15 @@ type ResolvedNonInteractiveApiKey = { source: "profile" | "env" | "flag"; }; +function hadStoredGigachatBasicProfile(): boolean { + const profile = loadAuthProfileStoreForSecretsRuntime().profiles["gigachat:default"]; + return ( + profile?.type === "api_key" && + profile.provider === "gigachat" && + profile.metadata?.authMode === "basic" + ); +} + async function applyGigachatNonInteractiveApiKeyChoice(params: { nextConfig: OpenClawConfig; baseConfig: OpenClawConfig; @@ -37,6 +48,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: { setter: (value: SecretInput) => Promise | void, ) => Promise; }): Promise { + const resetGigachatBaseUrl = hadStoredGigachatBasicProfile(); const resolved = await params.resolveApiKey({ provider: "gigachat", cfg: params.baseConfig, @@ -79,6 +91,7 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: { provider: "gigachat", mode: "api_key", }), + resetGigachatBaseUrl ? { baseUrl: GIGACHAT_BASE_URL } : undefined, ); }