GigaChat: distinguish Basic and OAuth credentials

This commit is contained in:
Alexander Davydov 2026-03-19 19:19:46 +03:00
parent 4ffdb2bb19
commit 2d2cdf24c2
5 changed files with 95 additions and 32 deletions

View File

@ -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");
});
});

View File

@ -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,
}),
});
}

View File

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

View File

@ -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);
});
});

View File

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