Skip to content

Commit a2aed9e

Browse files
committed
fix(channels): resolve /think menu levels against runtime catalog for live-discovered models (#93835)
Native /think menus on Telegram, Slack, and Discord resolved argument choices against the configured-only model catalog, so live-discovered reasoning models (e.g. Ollama glm-5.2:cloud) showed only default/off while /think <level> and the current-level title were correct. Load the runtime catalog for /think on all four native surfaces, including the default-model path when no /model override is set, so the menu matches the reply path. An empty/failed discovery keeps the configured-catalog fallback.
1 parent f9fc2ef commit a2aed9e

5 files changed

Lines changed: 90 additions & 1 deletion

File tree

extensions/discord/src/monitor/native-command.options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Discord plugin module implements native command.options behavior.
22
import { ApplicationCommandOptionType } from "discord-api-types/v10";
3+
import { loadModelCatalog } from "openclaw/plugin-sdk/agent-runtime";
34
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
45
import {
56
resolveCommandArgChoices,
@@ -117,12 +118,16 @@ export function buildDiscordCommandOptions(params: {
117118
? await resolveChoiceContext(interaction)
118119
: null;
119120
const currentCfg = resolveConfig?.() ?? cfg;
121+
// Load the runtime catalog for /think (default model can be live-discovered, e.g. Ollama reasoning); empty keeps the configured fallback.
122+
const choiceCatalog =
123+
command.key === "think" ? await loadModelCatalog({ config: currentCfg }) : undefined;
120124
const choices = resolveCommandArgChoices({
121125
command,
122126
arg,
123127
cfg: currentCfg,
124128
provider: context?.provider,
125129
model: context?.model,
130+
...(choiceCatalog?.length ? { catalog: choiceCatalog } : {}),
126131
});
127132
const filtered = focusValue
128133
? choices.filter((choice) =>

extensions/discord/src/monitor/native-command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Discord plugin module implements native command behavior.
22
import { ApplicationCommandOptionType } from "discord-api-types/v10";
3+
import { loadModelCatalog } from "openclaw/plugin-sdk/agent-runtime";
34
import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth-native";
45
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
56
import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime";
@@ -485,12 +486,16 @@ async function dispatchDiscordCommandInteraction(params: {
485486
threadBindings,
486487
})
487488
: null;
489+
// Load the runtime catalog for /think (default model can be live-discovered, e.g. Ollama reasoning); empty keeps the configured fallback.
490+
const menuModelCatalog =
491+
command.key === "think" ? await loadModelCatalog({ config: cfg }) : undefined;
488492
const menu = resolveCommandArgMenu({
489493
command,
490494
args: commandArgs,
491495
cfg,
492496
provider: menuModelContext?.provider,
493497
model: menuModelContext?.model,
498+
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
494499
});
495500
if (menu) {
496501
const menuPayload = buildDiscordCommandArgMenu({

extensions/slack/src/monitor/slash.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Slack plugin module implements slash behavior.
22
import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
3-
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
3+
import { loadModelCatalog, resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
44
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
55
import {
66
formatCommandArgMenuTitle,
@@ -596,11 +596,15 @@ export async function registerSlackMonitorSlashCommands(params: {
596596
sessionKey: menuRoute.sessionKey,
597597
})
598598
: {};
599+
// Load the runtime catalog for /think (default model can be live-discovered, e.g. Ollama reasoning); empty keeps the configured fallback.
600+
const menuModelCatalog =
601+
commandDefinition.key === "think" ? await loadModelCatalog({ config: cfg }) : undefined;
599602
const menu = resolveCommandArgMenu({
600603
command: commandDefinition,
601604
args: commandArgs,
602605
cfg,
603606
...menuModelContext,
607+
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
604608
});
605609
if (menu) {
606610
const commandLabel = commandDefinition.nativeName ?? commandDefinition.key;

extensions/telegram/src/bot-native-commands.session-meta.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,75 @@ describe("registerTelegramNativeCommands — session metadata", () => {
657657
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
658658
});
659659

660+
it("resolves /think menu choices against the runtime catalog for live-discovered models", async () => {
661+
// #93835: a wildcard-allowed Ollama model is reasoning-capable per live
662+
// /api/show discovery, but the configured catalog carries no reasoning flag.
663+
// The menu must resolve choices against the runtime catalog, not config-only.
664+
const cfg = {
665+
agents: { defaults: { models: { "ollama/*": {} } } },
666+
} as OpenClawConfig;
667+
sessionMocks.loadSessionStore.mockReturnValue({
668+
"agent:main:main": {
669+
providerOverride: "ollama",
670+
modelOverride: "glm-5.2:cloud",
671+
modelOverrideSource: "user",
672+
updatedAt: 0,
673+
},
674+
});
675+
const runtimeCatalog = [
676+
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
677+
];
678+
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
679+
680+
const { handler } = registerAndResolveCommandHandler({
681+
commandName: "think",
682+
cfg,
683+
allowFrom: ["*"],
684+
});
685+
await handler(createTelegramPrivateCommandContext());
686+
687+
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
688+
([params]) => params.command.key === "think" && params.provider === "ollama",
689+
)?.[0];
690+
const menuRecord = expectRecordFields(
691+
menuCall,
692+
{ provider: "ollama", model: "glm-5.2:cloud" },
693+
"ollama thinking menu call",
694+
);
695+
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
696+
expect(menuRecord.catalog).toEqual(runtimeCatalog);
697+
});
698+
699+
it("loads the runtime catalog for /think when no session model override is set", async () => {
700+
// #93835 default-model gap: when the agent default is a live-discovered
701+
// Ollama reasoning model and the user has not picked one via /model, the
702+
// menu context has no provider. The catalog must still load so the default
703+
// model's reasoning levels survive instead of the configured-only fallback.
704+
const cfg = {
705+
agents: { defaults: { model: "ollama/glm-5.2:cloud", models: { "ollama/*": {} } } },
706+
} as OpenClawConfig;
707+
sessionMocks.loadSessionStore.mockReturnValue({});
708+
const runtimeCatalog = [
709+
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
710+
];
711+
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
712+
713+
const { handler } = registerAndResolveCommandHandler({
714+
commandName: "think",
715+
cfg,
716+
allowFrom: ["*"],
717+
});
718+
await handler(createTelegramPrivateCommandContext());
719+
720+
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
721+
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
722+
([params]) => params.command.key === "think",
723+
)?.[0];
724+
const menuRecord = expectRecordFields(menuCall, {}, "default-model thinking menu call");
725+
expect(menuRecord.provider).toBeUndefined();
726+
expect(menuRecord.catalog).toEqual(runtimeCatalog);
727+
});
728+
660729
it("inherits the parent session model when building DM thread native argument menus", async () => {
661730
const cfg: OpenClawConfig = {};
662731
sessionMocks.loadSessionStore.mockReturnValue({

extensions/telegram/src/bot-native-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,12 +1135,18 @@ export const registerTelegramNativeCommands = ({
11351135
sessionKey: await resolveTargetSessionKey(),
11361136
})
11371137
: {};
1138+
// Load the runtime catalog for /think (default model can be live-discovered, e.g. Ollama reasoning); empty keeps the configured fallback.
1139+
const menuModelCatalog =
1140+
commandDefinition?.key === "think"
1141+
? await loadModelCatalog({ config: runtimeCfg })
1142+
: undefined;
11381143
const menu = commandDefinition
11391144
? resolveCommandArgMenu({
11401145
command: commandDefinition,
11411146
args: commandArgs,
11421147
cfg: runtimeCfg,
11431148
...menuModelContext,
1149+
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
11441150
})
11451151
: null;
11461152
if (menu && commandDefinition) {

0 commit comments

Comments
 (0)