GigaChat: avoid stale auth metadata fallback

This commit is contained in:
Alexander Davydov 2026-03-20 13:54:09 +03:00
parent 47355b93de
commit fb08d4e434
7 changed files with 133 additions and 5 deletions

View File

@ -5,10 +5,14 @@ export type GigachatAuthMetadata = Record<string, string> | undefined;
export function resolveGigachatAuthProfileMetadata(
store: Pick<AuthProfileStore, "profiles">,
authProfileId?: string,
options?: {
allowDefaultProfileFallback?: boolean;
},
): GigachatAuthMetadata {
const profileIds = [authProfileId?.trim(), "gigachat:default"].filter(
(profileId): profileId is string => Boolean(profileId),
);
const profileIds = [
authProfileId?.trim(),
options?.allowDefaultProfileFallback === false ? undefined : "gigachat:default",
].filter((profileId): profileId is string => Boolean(profileId));
for (const profileId of profileIds) {
const credential = store.profiles[profileId];
if (credential?.type === "api_key" && credential.provider === "gigachat") {
@ -39,7 +43,7 @@ export function resolveGigachatAuthMode(params: {
return metadataAuthMode;
}
if (!params.authProfileId?.trim() && looksLikeGigachatBasicCredentials(params.apiKey)) {
if (looksLikeGigachatBasicCredentials(params.apiKey)) {
return "basic";
}

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { GIGACHAT_BASE_URL, GIGACHAT_BASIC_BASE_URL } from "../commands/onboard-auth.models.js";
import { resolveGigachatAuthProfileMetadata } from "./gigachat-auth.js";
import { resolveImplicitGigachatBaseUrl } from "./models-config.providers.js";
describe("GigaChat implicit provider", () => {
@ -29,4 +30,30 @@ describe("GigaChat implicit provider", () => {
}),
).toBe("https://preview.gigachat.example/api/v1");
});
it("does not inherit stale default-profile metadata for auth-profile-less credentials", async () => {
const metadata = resolveGigachatAuthProfileMetadata(
{
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: {
authMode: "basic",
scope: "GIGACHAT_API_B2B",
},
},
},
},
undefined,
{ allowDefaultProfileFallback: false },
);
expect(
resolveImplicitGigachatBaseUrl({
metadata,
apiKey: "oauth:credential:with:colon",
}),
).toBe(GIGACHAT_BASE_URL);
});
});

View File

@ -737,7 +737,11 @@ function resolveImplicitGigachatProvider(ctx: ImplicitProviderContext): Provider
if (!auth.apiKey) {
return null;
}
const metadata = resolveGigachatAuthProfileMetadata(ctx.authStore, auth.profileId);
const metadata = resolveGigachatAuthProfileMetadata(ctx.authStore, auth.profileId, {
// Env-backed GIGACHAT_CREDENTIALS has no profile id, so do not inherit
// stale auth-mode metadata from a stored default profile.
allowDefaultProfileFallback: Boolean(auth.profileId?.trim()),
});
return buildGigachatProvider({
apiKey: auth.apiKey,

View File

@ -1064,6 +1064,66 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
expect(result.ok, result.reason).toBe(true);
});
it("does not inherit stale GigaChat metadata for env-backed OAuth credentials", async () => {
resolveModelMock.mockReturnValue({
model: {
provider: "gigachat",
api: "openai-completions",
id: "GigaChat-2-Max",
input: ["text"],
baseUrl: "https://gigachat.devices.sberbank.ru/api/v1",
},
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
} as never);
vi.mocked(getApiKeyForModel).mockResolvedValueOnce({
apiKey: "oauth:credential:with:colon",
mode: "api-key",
source: "env: GIGACHAT_CREDENTIALS",
});
ensureAuthProfileStoreMock.mockReturnValue({
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: {
authMode: "basic",
insecureTls: "true",
scope: "GIGACHAT_API_B2B",
},
},
},
});
sessionCompactImpl.mockImplementation(async () => {
expect(createGigachatStreamFnMock).toHaveBeenCalledWith({
baseUrl: "https://gigachat.devices.sberbank.ru/api/v1",
authMode: "oauth",
insecureTls: false,
scope: undefined,
});
return {
summary: "summary",
firstKeptEntryId: "entry-1",
tokensBefore: 120,
details: { ok: true },
};
});
const result = await compactEmbeddedPiSessionDirect({
sessionId: "session-1",
sessionKey: "agent:main:session-1",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: gigachatTestConfig(),
provider: "gigachat",
model: "GigaChat-2-Max",
customInstructions: "focus on decisions",
});
expect(result.ok, result.reason).toBe(true);
});
});
describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => {

View File

@ -856,6 +856,9 @@ export async function compactEmbeddedPiSessionDirect(
const gigachatMeta = resolveGigachatAuthProfileMetadata(
gigachatStore,
resolvedGigachatProfileId,
{
allowDefaultProfileFallback: Boolean(resolvedGigachatProfileId),
},
);
session.agent.streamFn = createGigachatStreamFn({

View File

@ -204,6 +204,24 @@ describe("resolveGigachatAuthProfileMetadata", () => {
),
).toEqual({ scope: "GIGACHAT_API_PERS" });
});
it("does not inherit the default GigaChat profile when fallback is disabled", () => {
expect(
resolveGigachatAuthProfileMetadata(
{
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: { scope: "GIGACHAT_API_B2B", insecureTls: "true" },
},
},
},
undefined,
{ allowDefaultProfileFallback: false },
),
).toBeUndefined();
});
});
describe("resolveGigachatAuthMode", () => {
@ -223,6 +241,15 @@ describe("resolveGigachatAuthMode", () => {
}),
).toBe("oauth");
});
it("infers basic auth for single-separator stored profile credentials without metadata", () => {
expect(
resolveGigachatAuthMode({
apiKey: "user:password",
authProfileId: "gigachat:default",
}),
).toBe("basic");
});
});
describe("composeSystemPromptWithHookContext", () => {

View File

@ -1982,6 +1982,9 @@ export async function runEmbeddedAttempt(
const gigachatMeta = resolveGigachatAuthProfileMetadata(
gigachatStore,
params.authProfileId,
{
allowDefaultProfileFallback: Boolean(params.authProfileId?.trim()),
},
);
const gigachatApiKey = await params.authStorage.getApiKey(params.provider);