Discord: use session model for /think autocomplete
This commit is contained in:
parent
c7134e629c
commit
d775c9499e
@ -5,12 +5,14 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
StringSelectMenu,
|
StringSelectMenu,
|
||||||
TextDisplay,
|
TextDisplay,
|
||||||
|
type AutocompleteInteraction,
|
||||||
type ButtonInteraction,
|
type ButtonInteraction,
|
||||||
type CommandInteraction,
|
type CommandInteraction,
|
||||||
type ComponentData,
|
type ComponentData,
|
||||||
type StringSelectMenuInteraction,
|
type StringSelectMenuInteraction,
|
||||||
} from "@buape/carbon";
|
} from "@buape/carbon";
|
||||||
import { ButtonStyle } from "discord-api-types/v10";
|
import { ButtonStyle } from "discord-api-types/v10";
|
||||||
|
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||||
import {
|
import {
|
||||||
buildCommandTextFromArgs,
|
buildCommandTextFromArgs,
|
||||||
findCommandByNativeName,
|
findCommandByNativeName,
|
||||||
@ -218,7 +220,11 @@ function buildDiscordModelPickerNoticePayload(message: string): { components: Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveDiscordModelPickerRoute(params: {
|
async function resolveDiscordModelPickerRoute(params: {
|
||||||
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
|
interaction:
|
||||||
|
| CommandInteraction
|
||||||
|
| ButtonInteraction
|
||||||
|
| StringSelectMenuInteraction
|
||||||
|
| AutocompleteInteraction;
|
||||||
cfg: ReturnType<typeof loadConfig>;
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
threadBindings: ThreadBindingManager;
|
threadBindings: ThreadBindingManager;
|
||||||
@ -269,6 +275,48 @@ async function resolveDiscordModelPickerRoute(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveDiscordNativeChoiceContext(params: {
|
||||||
|
interaction: AutocompleteInteraction;
|
||||||
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
|
accountId: string;
|
||||||
|
threadBindings: ThreadBindingManager;
|
||||||
|
}): Promise<{ provider?: string; model?: string } | null> {
|
||||||
|
try {
|
||||||
|
const route = await resolveDiscordModelPickerRoute({
|
||||||
|
interaction: params.interaction,
|
||||||
|
cfg: params.cfg,
|
||||||
|
accountId: params.accountId,
|
||||||
|
threadBindings: params.threadBindings,
|
||||||
|
});
|
||||||
|
const fallback = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId: route.agentId,
|
||||||
|
});
|
||||||
|
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||||
|
agentId: route.agentId,
|
||||||
|
});
|
||||||
|
const sessionStore = loadSessionStore(storePath);
|
||||||
|
const sessionEntry = sessionStore[route.sessionKey];
|
||||||
|
const override = resolveStoredModelOverride({
|
||||||
|
sessionEntry,
|
||||||
|
sessionStore,
|
||||||
|
sessionKey: route.sessionKey,
|
||||||
|
});
|
||||||
|
if (!override?.model) {
|
||||||
|
return {
|
||||||
|
provider: fallback.provider,
|
||||||
|
model: fallback.model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: override.provider || fallback.provider,
|
||||||
|
model: override.model,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDiscordModelPickerCurrentModel(params: {
|
function resolveDiscordModelPickerCurrentModel(params: {
|
||||||
cfg: ReturnType<typeof loadConfig>;
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
route: ResolvedAgentRoute;
|
route: ResolvedAgentRoute;
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { ChannelType, type AutocompleteInteraction } from "@buape/carbon";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { listNativeCommandSpecs } from "../../../../src/auto-reply/commands-registry.js";
|
||||||
|
import type { OpenClawConfig, loadConfig } from "../../../../src/config/config.js";
|
||||||
|
import { createDiscordNativeCommand } from "./native-command.js";
|
||||||
|
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
resolveBoundConversationRoute: vi.fn(),
|
||||||
|
loadSessionStore: vi.fn(),
|
||||||
|
resolveStorePath: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./route-resolution.js", () => ({
|
||||||
|
resolveDiscordBoundConversationRoute: mocks.resolveBoundConversationRoute,
|
||||||
|
resolveDiscordEffectiveRoute: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../../../src/config/sessions.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../../../../src/config/sessions.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadSessionStore: mocks.loadSessionStore,
|
||||||
|
resolveStorePath: mocks.resolveStorePath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("discord native /think autocomplete", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.resolveBoundConversationRoute.mockResolvedValue({
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "discord:session:1",
|
||||||
|
});
|
||||||
|
mocks.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions.mock.json");
|
||||||
|
mocks.loadSessionStore.mockReturnValue({
|
||||||
|
"discord:session:1": {
|
||||||
|
providerOverride: "openai-codex",
|
||||||
|
modelOverride: "gpt-5.4",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses bound session model override for /think choices", async () => {
|
||||||
|
const spec = listNativeCommandSpecs({ provider: "discord" }).find(
|
||||||
|
(entry) => entry.name === "think",
|
||||||
|
);
|
||||||
|
expect(spec).toBeTruthy();
|
||||||
|
if (!spec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-sonnet-4.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ReturnType<typeof loadConfig>;
|
||||||
|
const discordConfig = {} as NonNullable<OpenClawConfig["channels"]>["discord"];
|
||||||
|
const command = createDiscordNativeCommand({
|
||||||
|
command: spec,
|
||||||
|
cfg,
|
||||||
|
discordConfig,
|
||||||
|
accountId: "default",
|
||||||
|
sessionPrefix: "discord:slash",
|
||||||
|
ephemeralDefault: true,
|
||||||
|
threadBindings: createNoopThreadBindingManager("default"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const levelOption = command.options?.find((entry) => entry.name === "level") as
|
||||||
|
| {
|
||||||
|
autocomplete?: (
|
||||||
|
interaction: AutocompleteInteraction & {
|
||||||
|
respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
|
||||||
|
},
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
expect(typeof levelOption?.autocomplete).toBe("function");
|
||||||
|
if (typeof levelOption?.autocomplete !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const respond = vi.fn(async (_choices: Array<{ name: string; value: string }>) => {});
|
||||||
|
const interaction = {
|
||||||
|
options: {
|
||||||
|
getFocused: () => ({ value: "xh" }),
|
||||||
|
},
|
||||||
|
respond,
|
||||||
|
rawData: {},
|
||||||
|
channel: { id: "D1", type: ChannelType.DM },
|
||||||
|
user: { id: "U1" },
|
||||||
|
guild: undefined,
|
||||||
|
client: {},
|
||||||
|
} as unknown as AutocompleteInteraction & {
|
||||||
|
respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
await levelOption.autocomplete(interaction);
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledTimes(1);
|
||||||
|
const choices = respond.mock.calls[0]?.[0] ?? [];
|
||||||
|
const values = choices.map((choice) => choice.value);
|
||||||
|
expect(values).toContain("xhigh");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -73,6 +73,7 @@ import {
|
|||||||
createDiscordModelPickerFallbackButton as createDiscordModelPickerFallbackButtonUi,
|
createDiscordModelPickerFallbackButton as createDiscordModelPickerFallbackButtonUi,
|
||||||
createDiscordModelPickerFallbackSelect as createDiscordModelPickerFallbackSelectUi,
|
createDiscordModelPickerFallbackSelect as createDiscordModelPickerFallbackSelectUi,
|
||||||
replyWithDiscordModelPickerProviders,
|
replyWithDiscordModelPickerProviders,
|
||||||
|
resolveDiscordNativeChoiceContext,
|
||||||
shouldOpenDiscordModelPickerFromCommand,
|
shouldOpenDiscordModelPickerFromCommand,
|
||||||
type DiscordCommandArgContext,
|
type DiscordCommandArgContext,
|
||||||
type DiscordModelPickerContext,
|
type DiscordModelPickerContext,
|
||||||
@ -124,8 +125,11 @@ function resolveDiscordNativeCommandAllowlistAccess(params: {
|
|||||||
function buildDiscordCommandOptions(params: {
|
function buildDiscordCommandOptions(params: {
|
||||||
command: ChatCommandDefinition;
|
command: ChatCommandDefinition;
|
||||||
cfg: ReturnType<typeof loadConfig>;
|
cfg: ReturnType<typeof loadConfig>;
|
||||||
|
resolveChoiceContext?: (
|
||||||
|
interaction: AutocompleteInteraction,
|
||||||
|
) => Promise<{ provider?: string; model?: string } | null>;
|
||||||
}): CommandOptions | undefined {
|
}): CommandOptions | undefined {
|
||||||
const { command, cfg } = params;
|
const { command, cfg, resolveChoiceContext } = params;
|
||||||
const args = command.args;
|
const args = command.args;
|
||||||
if (!args || args.length === 0) {
|
if (!args || args.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -158,7 +162,17 @@ function buildDiscordCommandOptions(params: {
|
|||||||
const focused = interaction.options.getFocused();
|
const focused = interaction.options.getFocused();
|
||||||
const focusValue =
|
const focusValue =
|
||||||
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
|
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
|
||||||
const choices = resolveCommandArgChoices({ command, arg, cfg });
|
const context =
|
||||||
|
typeof arg.choices === "function" && resolveChoiceContext
|
||||||
|
? await resolveChoiceContext(interaction)
|
||||||
|
: null;
|
||||||
|
const choices = resolveCommandArgChoices({
|
||||||
|
command,
|
||||||
|
arg,
|
||||||
|
cfg,
|
||||||
|
provider: context?.provider,
|
||||||
|
model: context?.model,
|
||||||
|
});
|
||||||
const filtered = focusValue
|
const filtered = focusValue
|
||||||
? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
|
? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
|
||||||
: choices;
|
: choices;
|
||||||
@ -297,6 +311,13 @@ export function createDiscordNativeCommand(params: {
|
|||||||
const commandOptions = buildDiscordCommandOptions({
|
const commandOptions = buildDiscordCommandOptions({
|
||||||
command: commandDefinition,
|
command: commandDefinition,
|
||||||
cfg,
|
cfg,
|
||||||
|
resolveChoiceContext: async (interaction) =>
|
||||||
|
resolveDiscordNativeChoiceContext({
|
||||||
|
interaction,
|
||||||
|
cfg,
|
||||||
|
accountId,
|
||||||
|
threadBindings,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const options = commandOptions
|
const options = commandOptions
|
||||||
? (commandOptions satisfies CommandOptions)
|
? (commandOptions satisfies CommandOptions)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user