mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 02:12:44 +00:00
Mattermost: add interactive model picker (#38767)
Merged via squash.
Prepared head SHA: 0883654e88
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm
This commit is contained in:
committed by
GitHub
parent
33e7394861
commit
4f08dcccfd
@@ -1,12 +1,11 @@
|
||||
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||
import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
|
||||
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
buildModelAliasIndex,
|
||||
normalizeProviderId,
|
||||
resolveConfiguredModelRef,
|
||||
resolveDefaultModelForAgent,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -35,11 +34,13 @@ export type ModelsProviderData = {
|
||||
* Build provider/model data from config and catalog.
|
||||
* Exported for reuse by callback handlers.
|
||||
*/
|
||||
export async function buildModelsProviderData(cfg: OpenClawConfig): Promise<ModelsProviderData> {
|
||||
const resolvedDefault = resolveConfiguredModelRef({
|
||||
export async function buildModelsProviderData(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): Promise<ModelsProviderData> {
|
||||
const resolvedDefault = resolveDefaultModelForAgent({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
agentId,
|
||||
});
|
||||
|
||||
const catalog = await loadModelCatalog({ config: cfg });
|
||||
@@ -220,6 +221,7 @@ export async function resolveModelsCommandReply(params: {
|
||||
commandBodyNormalized: string;
|
||||
surface?: string;
|
||||
currentModel?: string;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
sessionEntry?: SessionEntry;
|
||||
}): Promise<ReplyPayload | null> {
|
||||
@@ -231,7 +233,7 @@ export async function resolveModelsCommandReply(params: {
|
||||
const argText = body.replace(/^\/models\b/i, "").trim();
|
||||
const { provider, page, pageSize, all } = parseModelsArgs(argText);
|
||||
|
||||
const { byProvider, providers } = await buildModelsProviderData(params.cfg);
|
||||
const { byProvider, providers } = await buildModelsProviderData(params.cfg, params.agentId);
|
||||
const isTelegram = params.surface === "telegram";
|
||||
|
||||
// Provider list (no provider specified)
|
||||
@@ -386,6 +388,7 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma
|
||||
commandBodyNormalized,
|
||||
surface: params.ctx.Surface,
|
||||
currentModel: params.model ? `${params.provider}/${params.model}` : undefined,
|
||||
agentId: modelsAgentId,
|
||||
agentDir: modelsAgentDir,
|
||||
sessionEntry: params.sessionEntry,
|
||||
});
|
||||
|
||||
@@ -907,6 +907,28 @@ describe("/models command", () => {
|
||||
expect(result.reply?.text).toContain("localai/ultra-chat");
|
||||
expect(result.reply?.text).not.toContain("Unknown provider");
|
||||
});
|
||||
|
||||
it("threads the routed agent through /models replies", async () => {
|
||||
const scopedCfg = {
|
||||
commands: { text: true },
|
||||
agents: {
|
||||
defaults: { model: { primary: "anthropic/claude-opus-4-5" } },
|
||||
list: [{ id: "support", model: "localai/ultra-chat" }],
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const params = buildPolicyParams("/models", scopedCfg, {
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
});
|
||||
|
||||
const result = await handleCommands({
|
||||
...params,
|
||||
agentId: "support",
|
||||
sessionKey: "agent:support:main",
|
||||
});
|
||||
|
||||
expect(result.reply?.text).toContain("localai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleCommands plugin commands", () => {
|
||||
|
||||
@@ -61,15 +61,17 @@ function renderRecentsViewRows(
|
||||
}
|
||||
|
||||
describe("loadDiscordModelPickerData", () => {
|
||||
it("reuses buildModelsProviderData as source of truth", async () => {
|
||||
it("reuses buildModelsProviderData as source of truth with agent scope", async () => {
|
||||
const expected = createModelsProviderData({ openai: ["gpt-4o"] });
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const spy = vi
|
||||
.spyOn(modelsCommandModule, "buildModelsProviderData")
|
||||
.mockResolvedValue(expected);
|
||||
|
||||
const result = await loadDiscordModelPickerData({} as OpenClawConfig);
|
||||
const result = await loadDiscordModelPickerData(cfg, "support");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(cfg, "support");
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,8 +541,11 @@ function buildModelRows(params: {
|
||||
* Source-of-truth data for Discord picker views. This intentionally reuses the
|
||||
* same provider/model resolver used by text and Telegram model commands.
|
||||
*/
|
||||
export async function loadDiscordModelPickerData(cfg: OpenClawConfig): Promise<ModelsProviderData> {
|
||||
return buildModelsProviderData(cfg);
|
||||
export async function loadDiscordModelPickerData(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
): Promise<ModelsProviderData> {
|
||||
return buildModelsProviderData(cfg, agentId);
|
||||
}
|
||||
|
||||
export function buildDiscordModelPickerCustomId(params: {
|
||||
|
||||
@@ -476,13 +476,13 @@ async function replyWithDiscordModelPickerProviders(params: {
|
||||
threadBindings: ThreadBindingManager;
|
||||
preferFollowUp: boolean;
|
||||
}) {
|
||||
const data = await loadDiscordModelPickerData(params.cfg);
|
||||
const route = await resolveDiscordModelPickerRoute({
|
||||
interaction: params.interaction,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
threadBindings: params.threadBindings,
|
||||
});
|
||||
const data = await loadDiscordModelPickerData(params.cfg, route.agentId);
|
||||
const currentModel = resolveDiscordModelPickerCurrentModel({
|
||||
cfg: params.cfg,
|
||||
route,
|
||||
@@ -637,13 +637,13 @@ async function handleDiscordModelPickerInteraction(
|
||||
return;
|
||||
}
|
||||
|
||||
const pickerData = await loadDiscordModelPickerData(ctx.cfg);
|
||||
const route = await resolveDiscordModelPickerRoute({
|
||||
interaction,
|
||||
cfg: ctx.cfg,
|
||||
accountId: ctx.accountId,
|
||||
threadBindings: ctx.threadBindings,
|
||||
});
|
||||
const pickerData = await loadDiscordModelPickerData(ctx.cfg, route.agentId);
|
||||
const currentModelRef = resolveDiscordModelPickerCurrentModel({
|
||||
cfg: ctx.cfg,
|
||||
route,
|
||||
|
||||
@@ -15,6 +15,12 @@ export type { ChatType } from "../channels/chat-type.js";
|
||||
export { resolveControlCommandGate } from "../channels/command-gating.js";
|
||||
export { logInboundDrop, logTypingFailure } from "../channels/logging.js";
|
||||
export { resolveAllowlistMatchSimple } from "../channels/plugins/allowlist-match.js";
|
||||
export { normalizeProviderId } from "../agents/model-selection.js";
|
||||
export {
|
||||
buildModelsProviderData,
|
||||
type ModelsProviderData,
|
||||
} from "../auto-reply/reply/commands-models.js";
|
||||
export { resolveStoredModelOverride } from "../auto-reply/reply/model-selection.js";
|
||||
export {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
@@ -44,6 +50,7 @@ export { createReplyPrefixOptions } from "../channels/reply-prefix.js";
|
||||
export { createTypingCallbacks } from "../channels/typing.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js";
|
||||
export { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
@@ -65,6 +72,7 @@ export {
|
||||
} from "../config/zod-schema.core.js";
|
||||
export { createDedupeCache } from "../infra/dedupe.js";
|
||||
export { rawDataToString } from "../infra/ws.js";
|
||||
export { isLoopbackHost, isTrustedProxyAddress, resolveClientIp } from "../gateway/net.js";
|
||||
export { registerPluginHttpRoute } from "../plugins/http-registry.js";
|
||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
|
||||
@@ -1179,7 +1179,15 @@ export const registerTelegramHandlers = ({
|
||||
// Model selection callback handler (mdl_prov, mdl_list_*, mdl_sel_*, mdl_back)
|
||||
const modelCallback = parseModelCallbackData(data);
|
||||
if (modelCallback) {
|
||||
const modelData = await buildModelsProviderData(cfg);
|
||||
const sessionState = resolveTelegramSessionState({
|
||||
chatId,
|
||||
isGroup,
|
||||
isForum,
|
||||
messageThreadId,
|
||||
resolvedThreadId,
|
||||
senderId,
|
||||
});
|
||||
const modelData = await buildModelsProviderData(cfg, sessionState.agentId);
|
||||
const { byProvider, providers } = modelData;
|
||||
|
||||
const editMessageWithButtons = async (
|
||||
@@ -1238,14 +1246,15 @@ export const registerTelegramHandlers = ({
|
||||
const safePage = Math.max(1, Math.min(page, totalPages));
|
||||
|
||||
// Resolve current model from session (prefer overrides)
|
||||
const sessionState = resolveTelegramSessionState({
|
||||
const currentSessionState = resolveTelegramSessionState({
|
||||
chatId,
|
||||
isGroup,
|
||||
isForum,
|
||||
messageThreadId,
|
||||
resolvedThreadId,
|
||||
senderId,
|
||||
});
|
||||
const currentModel = sessionState.model;
|
||||
const currentModel = currentSessionState.model;
|
||||
|
||||
const buttons = buildModelsKeyboard({
|
||||
provider,
|
||||
@@ -1259,8 +1268,8 @@ export const registerTelegramHandlers = ({
|
||||
provider,
|
||||
total: models.length,
|
||||
cfg,
|
||||
agentDir: resolveAgentDir(cfg, sessionState.agentId),
|
||||
sessionEntry: sessionState.sessionEntry,
|
||||
agentDir: resolveAgentDir(cfg, currentSessionState.agentId),
|
||||
sessionEntry: currentSessionState.sessionEntry,
|
||||
});
|
||||
await editMessageWithButtons(text, buttons);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user