From 2d2cdf24c2705c8ae353fcc92dc3b3d5bb72f777 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Thu, 19 Mar 2026 19:19:46 +0300 Subject: [PATCH] GigaChat: distinguish Basic and OAuth credentials --- .../models-config.providers.gigachat.test.ts | 48 +++++++------------ src/agents/models-config.providers.ts | 33 ++++++++++++- src/commands/onboard-auth.models.ts | 1 + .../auth-choice.api-key-providers.test.ts | 33 +++++++++++++ .../local/auth-choice.api-key-providers.ts | 12 +++++ 5 files changed, 95 insertions(+), 32 deletions(-) diff --git a/src/agents/models-config.providers.gigachat.test.ts b/src/agents/models-config.providers.gigachat.test.ts index 44ab4dc71dc..f9a98661aba 100644 --- a/src/agents/models-config.providers.gigachat.test.ts +++ b/src/agents/models-config.providers.gigachat.test.ts @@ -1,40 +1,26 @@ -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { withEnvAsync } from "../test-utils/env.js"; -import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js"; +import { GIGACHAT_BASE_URL, GIGACHAT_BASIC_BASE_URL } from "../commands/onboard-auth.models.js"; +import { resolveImplicitGigachatBaseUrl } from "./models-config.providers.js"; describe("GigaChat implicit provider", () => { - it("injects the default provider when GIGACHAT_CREDENTIALS is configured", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + it("uses the Basic default host for implicit Basic credentials", async () => { + expect(resolveImplicitGigachatBaseUrl({ apiKey: "user:password" })).toBe( + GIGACHAT_BASIC_BASE_URL, + ); + }); - await withEnvAsync({ GIGACHAT_CREDENTIALS: "user:password" }, async () => { - const providers = await resolveImplicitProvidersForTest({ agentDir }); - - expect(providers?.gigachat).toMatchObject({ - baseUrl: "https://gigachat.devices.sberbank.ru/api/v1", - api: "openai-completions", - apiKey: "GIGACHAT_CREDENTIALS", - }); - expect(providers?.gigachat?.models?.map((model) => model.id)).toEqual(["GigaChat-2-Max"]); - }); + it("keeps the OAuth default host for implicit OAuth credentials keys", async () => { + expect(resolveImplicitGigachatBaseUrl({ apiKey: "oauth-credentials-key" })).toBe( + GIGACHAT_BASE_URL, + ); }); it("honors GIGACHAT_BASE_URL for implicit providers", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - - await withEnvAsync( - { - GIGACHAT_CREDENTIALS: "user:password", - GIGACHAT_BASE_URL: "https://preview.gigachat.example/api/v1", - }, - async () => { - const providers = await resolveImplicitProvidersForTest({ agentDir }); - - expect(providers?.gigachat?.baseUrl).toBe("https://preview.gigachat.example/api/v1"); - expect(providers?.gigachat?.apiKey).toBe("GIGACHAT_CREDENTIALS"); - }, - ); + expect( + resolveImplicitGigachatBaseUrl({ + apiKey: "user:password", + envBaseUrl: "https://preview.gigachat.example/api/v1", + }), + ).toBe("https://preview.gigachat.example/api/v1"); }); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index e9965494b7c..2d84f7f1b04 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -4,6 +4,7 @@ import { } from "../../extensions/qianfan/provider-catalog.js"; import { XIAOMI_DEFAULT_MODEL_ID } from "../../extensions/xiaomi/provider-catalog.js"; import { + GIGACHAT_BASIC_BASE_URL, buildGigachatModelDefinition, GIGACHAT_BASE_URL, } from "../commands/onboard-auth.models.js"; @@ -13,6 +14,11 @@ import { isRecord } from "../utils.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { discoverBedrockModels } from "./bedrock-discovery.js"; +import { + type GigachatAuthMetadata, + resolveGigachatAuthMode, + resolveGigachatAuthProfileMetadata, +} from "./gigachat-auth.js"; import { normalizeGoogleModelId } from "./model-id-normalization.js"; import { resolveOllamaApiBase } from "./models-config.providers.discovery.js"; export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js"; @@ -97,6 +103,25 @@ function buildGigachatProvider(params: { apiKey?: string; baseUrl?: string }): P } satisfies ProviderConfig; } +export function resolveImplicitGigachatBaseUrl(params: { + envBaseUrl?: string; + metadata?: GigachatAuthMetadata; + apiKey?: string; + authProfileId?: string; +}): string { + const envBaseUrl = params.envBaseUrl?.trim(); + if (envBaseUrl) { + return envBaseUrl; + } + return resolveGigachatAuthMode({ + metadata: params.metadata, + apiKey: params.apiKey, + authProfileId: params.authProfileId, + }) === "basic" + ? GIGACHAT_BASIC_BASE_URL + : GIGACHAT_BASE_URL; +} + function withStreamingUsageCompat(provider: ProviderConfig): ProviderConfig { if (!Array.isArray(provider.models) || provider.models.length === 0) { return provider; @@ -712,10 +737,16 @@ function resolveImplicitGigachatProvider(ctx: ImplicitProviderContext): Provider if (!auth.apiKey) { return null; } + const metadata = resolveGigachatAuthProfileMetadata(ctx.authStore, auth.profileId); return buildGigachatProvider({ apiKey: auth.apiKey, - baseUrl: ctx.env.GIGACHAT_BASE_URL, + baseUrl: resolveImplicitGigachatBaseUrl({ + envBaseUrl: ctx.env.GIGACHAT_BASE_URL, + metadata, + apiKey: auth.discoveryApiKey ?? auth.apiKey, + authProfileId: auth.profileId, + }), }); } diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index 882c6faf7eb..4a54415216a 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -218,6 +218,7 @@ export function buildXaiModelDefinition(): ModelDefinitionConfig { } export const GIGACHAT_BASE_URL = "https://gigachat.devices.sberbank.ru/api/v1"; +export const GIGACHAT_BASIC_BASE_URL = "https://gigachat.ift.sberdevices.ru/v1"; export const GIGACHAT_DEFAULT_MODEL_ID = "GigaChat-2-Max"; export const GIGACHAT_DEFAULT_MODEL_REF = `gigachat/${GIGACHAT_DEFAULT_MODEL_ID}`; export const GIGACHAT_DEFAULT_CONTEXT_WINDOW = 128000; 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 1352038499a..0944054cc20 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 @@ -117,4 +117,37 @@ describe("applySimpleNonInteractiveApiKeyChoice", () => { }, ); }); + + it("rejects Basic-shaped GIGACHAT_CREDENTIALS in the OAuth onboarding path", async () => { + const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; + const runtime = { + error: vi.fn(), + exit: vi.fn(), + log: vi.fn(), + } as never; + const resolveApiKey = vi.fn(async () => ({ + key: "basic-user:basic-pass", + source: "env" as const, + })); + const maybeSetResolvedApiKey = vi.fn(); + + const result = await applySimpleNonInteractiveApiKeyChoice({ + authChoice: "gigachat-api-key", + nextConfig, + baseConfig: nextConfig, + opts: {} as never, + runtime, + apiKeyStorageOptions: undefined, + resolveApiKey, + maybeSetResolvedApiKey, + }); + + expect(result).toBeNull(); + expect(maybeSetResolvedApiKey).not.toHaveBeenCalled(); + expect(setGigachatApiKey).not.toHaveBeenCalled(); + expect(runtime.error).toHaveBeenCalledWith( + expect.stringContaining("Basic user:password credentials"), + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + }); }); 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 fef8693588e..e8a86cebbcf 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 { resolveGigachatAuthMode } from "../../../agents/gigachat-auth.js"; import type { OpenClawConfig } from "../../../config/config.js"; import type { SecretInput } from "../../../config/types.secrets.js"; import { applyAuthProfileConfig } from "../../../plugins/provider-auth-helpers.js"; @@ -50,6 +51,17 @@ async function applyGigachatNonInteractiveApiKeyChoice(params: { if (!resolved) { return null; } + if (resolveGigachatAuthMode({ apiKey: resolved.key }) === "basic") { + params.runtime.error( + [ + "GIGACHAT_CREDENTIALS looks like Basic user:password credentials.", + 'Non-interactive "--gigachat-api-key" only supports personal OAuth credentials keys.', + "Set GIGACHAT_CREDENTIALS to a real OAuth credentials key and retry.", + ].join("\n"), + ); + params.runtime.exit(1); + return null; + } if ( !(await params.maybeSetResolvedApiKey(resolved, (value) => setGigachatApiKey(value, undefined, params.apiKeyStorageOptions, {