GigaChat: respect runtime auth metadata

This commit is contained in:
Alexander Davydov 2026-03-16 14:25:20 +03:00
parent 9134eb7af2
commit 95a75ffe01
5 changed files with 110 additions and 3 deletions

View File

@ -467,4 +467,17 @@ describe("getApiKeyForModel", () => {
},
);
});
it("resolveEnvApiKey('gigachat') ignores password-only basic-auth envs", async () => {
await withEnvAsync(
{
GIGACHAT_CREDENTIALS: undefined,
GIGACHAT_PASSWORD: "gigachat-basic-password",
},
async () => {
const resolved = resolveEnvApiKey("gigachat");
expect(resolved).toBeUndefined();
},
);
});
});

View File

@ -6,6 +6,7 @@ import {
composeSystemPromptWithHookContext,
isOllamaCompatProvider,
prependSystemPromptAddition,
resolveGigachatAuthProfileMetadata,
resolveAttemptFsWorkspaceOnly,
resolveOllamaCompatNumCtxEnabled,
resolvePromptBuildHookResult,
@ -106,6 +107,69 @@ describe("resolvePromptBuildHookResult", () => {
});
});
describe("resolveGigachatAuthProfileMetadata", () => {
it("prefers the active GigaChat auth profile metadata over the default profile", () => {
expect(
resolveGigachatAuthProfileMetadata(
{
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: { scope: "GIGACHAT_API_PERS", insecureTls: "false" },
},
"gigachat:business": {
type: "api_key",
provider: "gigachat",
metadata: { scope: "GIGACHAT_API_B2B", insecureTls: "true" },
},
},
},
"gigachat:business",
),
).toEqual({ scope: "GIGACHAT_API_B2B", insecureTls: "true" });
});
it("falls back to the default GigaChat profile metadata when the active profile is absent", () => {
expect(
resolveGigachatAuthProfileMetadata(
{
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: { scope: "GIGACHAT_API_PERS", insecureTls: "false" },
},
},
},
"gigachat:business",
),
).toEqual({ scope: "GIGACHAT_API_PERS", insecureTls: "false" });
});
it("ignores non-GigaChat active profiles when resolving metadata", () => {
expect(
resolveGigachatAuthProfileMetadata(
{
profiles: {
"gigachat:default": {
type: "api_key",
provider: "gigachat",
metadata: { scope: "GIGACHAT_API_PERS" },
},
"openai:p1": {
type: "api_key",
provider: "openai",
metadata: { scope: "not-gigachat" },
},
},
},
"openai:p1",
),
).toEqual({ scope: "GIGACHAT_API_PERS" });
});
});
describe("composeSystemPromptWithHookContext", () => {
it("returns undefined when no hook system context is provided", () => {
expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined();

View File

@ -35,6 +35,7 @@ import { resolveOpenClawAgentDir } from "../../agent-paths.js";
import { resolveSessionAgentIds } from "../../agent-scope.js";
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
import { ensureAuthProfileStore } from "../../auth-profiles.js";
import type { ApiKeyCredential, AuthProfileStore } from "../../auth-profiles.js";
import {
analyzeBootstrapBudget,
buildBootstrapPromptWarning,
@ -218,6 +219,25 @@ function createYieldAbortedResponse(model: { api?: string; provider?: string; id
};
}
export function resolveGigachatAuthProfileMetadata(
store: Pick<AuthProfileStore, "profiles">,
authProfileId?: string,
): Record<string, string> | undefined {
const profileIds = [authProfileId?.trim(), "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 as ApiKeyCredential).provider === "gigachat"
) {
return credential.metadata;
}
}
return undefined;
}
// Queue a hidden steering message so pi-agent-core skips any remaining tool calls.
function queueSessionsYieldInterruptMessage(activeSession: {
agent: { steer: (message: AgentMessage) => void };
@ -1913,8 +1933,10 @@ export async function runEmbeddedAttempt(
// Read GigaChat-specific config from auth profile credential metadata.
const gigachatStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const gigachatCred = gigachatStore.profiles["gigachat:default"];
const gigachatMeta = gigachatCred?.type === "api_key" ? gigachatCred.metadata : undefined;
const gigachatMeta = resolveGigachatAuthProfileMetadata(
gigachatStore,
params.attempt.authProfileId,
);
const gigachatStreamFn = createGigachatStreamFn({
baseUrl,

View File

@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
import {
PROVIDER_AUTH_ENV_VAR_CANDIDATES,
PROVIDER_ENV_VARS,
listKnownProviderAuthEnvVarNames,
listKnownSecretEnvVarNames,
omitEnvKeysCaseInsensitive,
@ -42,4 +44,9 @@ describe("provider env vars", () => {
expect.arrayContaining(["GIGACHAT_CREDENTIALS", "GIGACHAT_PASSWORD"]),
);
});
it("does not treat GigaChat password-only env vars as API-key candidates", () => {
expect(PROVIDER_AUTH_ENV_VAR_CANDIDATES.gigachat).toEqual(["GIGACHAT_CREDENTIALS"]);
expect(PROVIDER_ENV_VARS.gigachat).toEqual(["GIGACHAT_CREDENTIALS", "GIGACHAT_PASSWORD"]);
});
});

View File

@ -15,7 +15,7 @@ const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
qianfan: ["QIANFAN_API_KEY"],
xai: ["XAI_API_KEY"],
mistral: ["MISTRAL_API_KEY"],
gigachat: ["GIGACHAT_CREDENTIALS", "GIGACHAT_PASSWORD"],
gigachat: ["GIGACHAT_CREDENTIALS"],
kilocode: ["KILOCODE_API_KEY"],
modelstudio: ["MODELSTUDIO_API_KEY"],
volcengine: ["VOLCANO_ENGINE_API_KEY"],
@ -25,6 +25,7 @@ const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
gigachat: ["GIGACHAT_CREDENTIALS", "GIGACHAT_PASSWORD"],
"minimax-cn": ["MINIMAX_API_KEY"],
} as const;