GigaChat: fix basic scope and Telegram picker callbacks

This commit is contained in:
Alexander Davydov 2026-03-19 19:54:45 +03:00
parent 2c4f5203f5
commit fd971e5b79
4 changed files with 91 additions and 13 deletions

View File

@ -1342,8 +1342,14 @@ export const registerTelegramHandlers = ({
resolvedThreadId,
senderId,
});
const configuredModelData = buildConfiguredModelsProviderData(cfg, sessionState.agentId);
let modelData = configuredModelData;
const configuredModelData = buildConfiguredModelsProviderData(
runtimeCfg,
sessionState.agentId,
);
let modelData = await telegramDeps.buildModelsProviderData(
runtimeCfg,
sessionState.agentId,
);
let { byProvider, providers } = modelData;
const editMessageWithButtons = async (
@ -1434,12 +1440,12 @@ export const registerTelegramHandlers = ({
if (modelCallback.type === "select") {
let selection = resolveModelSelection({
callback: modelCallback,
providers,
byProvider,
providers: configuredModelData.providers,
byProvider: configuredModelData.byProvider,
});
let selectionAllowed =
selection.kind === "resolved" &&
Boolean(byProvider.get(selection.provider)?.has(selection.model));
Boolean(configuredModelData.byProvider.get(selection.provider)?.has(selection.model));
if (!selectionAllowed || selection.kind !== "resolved") {
modelData = await telegramDeps.buildModelsProviderData(

View File

@ -197,7 +197,31 @@ describe("createTelegramBot", () => {
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-1");
});
it("reloads callback model routing bindings without recreating the bot", async () => {
const buildModelsProviderDataMock =
telegramBotDepsForTest.buildModelsProviderData as unknown as ReturnType<typeof vi.fn>;
let boundAgentId = "agent-a";
buildModelsProviderDataMock.mockImplementation(
async (_cfg: OpenClawConfig, agentId?: string) => {
if (agentId === "agent-b") {
return {
byProvider: new Map([
["anthropic", new Set(["claude-opus-4-5"])],
["openai", new Set(["gpt-4.1"])],
]),
providers: ["anthropic", "openai"],
resolvedDefault: { provider: "anthropic", model: "claude-opus-4-5" },
};
}
return {
byProvider: new Map([
["gemini", new Set(["gemini-2.5-pro"])],
["openai", new Set(["gpt-4.1"])],
]),
providers: ["gemini", "openai"],
resolvedDefault: { provider: "openai", model: "gpt-4.1" },
};
},
);
loadConfig.mockImplementation(() => ({
agents: {
defaults: {
@ -238,20 +262,32 @@ describe("createTelegramBot", () => {
});
};
buildModelsProviderDataMock.mockClear();
editMessageTextSpy.mockClear();
await sendModelCallback(1);
expect(buildModelsProviderDataMock).toHaveBeenCalled();
expect(buildModelsProviderDataMock.mock.calls.at(-1)?.[1]).toBe("agent-a");
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
expect(editMessageTextSpy.mock.calls.at(-1)?.[3]).toEqual(
expect.objectContaining({
reply_markup: {
inline_keyboard: [[{ text: "openai (1)", callback_data: "mdl_list_openai_1" }]],
},
}),
expect(
editMessageTextSpy.mock.calls.at(-1)?.[3]?.reply_markup?.inline_keyboard?.flat(),
).toEqual(
expect.arrayContaining([
expect.objectContaining({
text: "gemini (1)",
callback_data: "mdl_list_gemini_1",
}),
expect.objectContaining({
text: "openai (1)",
callback_data: "mdl_list_openai_1",
}),
]),
);
boundAgentId = "agent-b";
buildModelsProviderDataMock.mockClear();
editMessageTextSpy.mockClear();
await sendModelCallback(2);
expect(buildModelsProviderDataMock.mock.calls.at(-1)?.[1]).toBe("agent-b");
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
expect(
editMessageTextSpy.mock.calls.at(-1)?.[3]?.reply_markup?.inline_keyboard?.flat(),

View File

@ -653,6 +653,37 @@ describe("createGigachatStreamFn tool calling", () => {
expect(clientConfigs[0]?.password).toBeUndefined();
});
it("honors explicit basic auth scopes when metadata provides one", async () => {
request.mockResolvedValueOnce({
status: 200,
data: createSseStream(['data: {"choices":[{"delta":{"content":"done"}}]}', "data: [DONE]"]),
});
const streamFn = createGigachatStreamFn({
baseUrl: "https://gigachat.ift.sberdevices.ru/v1",
authMode: "basic",
scope: "GIGACHAT_API_B2B",
});
const stream = await streamFn(
{ api: "gigachat", provider: "gigachat", id: "GigaChat-2-Max" } as never,
{ messages: [], tools: [] } as never,
{ apiKey: "basic-user:basic-password" } as never,
);
await expect(stream.result()).resolves.toMatchObject({
content: [{ type: "text", text: "done" }],
});
expect(clientConfigs).toHaveLength(1);
expect(clientConfigs[0]).toMatchObject({
user: "basic-user",
password: "basic-password",
scope: "GIGACHAT_API_B2B",
});
expect(clientConfigs[0]?.credentials).toBeUndefined();
});
it("falls back to the SDK default oauth scope when no metadata scope is available", async () => {
request.mockResolvedValueOnce({
status: 200,

View File

@ -544,6 +544,7 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
profanityCheck: undefined,
timeout: 120,
};
const configuredScope = opts.scope?.trim();
if (insecureTls) {
clientConfig.httpsAgent = new https.Agent({ rejectUnauthorized: false });
@ -553,10 +554,14 @@ export function createGigachatStreamFn(opts: GigachatStreamOptions): StreamFn {
const { user, password } = parseGigachatBasicCredentials(apiKey);
clientConfig.user = user;
clientConfig.password = password;
log.debug(`GigaChat auth: basic mode`);
if (configuredScope) {
clientConfig.scope = configuredScope;
}
log.debug(
`GigaChat auth: basic mode${clientConfig.scope ? ` scope=${clientConfig.scope}` : ""}`,
);
} else {
clientConfig.credentials = apiKey;
const configuredScope = opts.scope?.trim();
if (configuredScope) {
clientConfig.scope = configuredScope;
}