Discord: use session model for /think autocomplete
This commit is contained in:
parent
c7134e629c
commit
d775c9499e
@ -5,12 +5,14 @@ import {
|
||||
Row,
|
||||
StringSelectMenu,
|
||||
TextDisplay,
|
||||
type AutocompleteInteraction,
|
||||
type ButtonInteraction,
|
||||
type CommandInteraction,
|
||||
type ComponentData,
|
||||
type StringSelectMenuInteraction,
|
||||
} from "@buape/carbon";
|
||||
import { ButtonStyle } from "discord-api-types/v10";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
buildCommandTextFromArgs,
|
||||
findCommandByNativeName,
|
||||
@ -218,7 +220,11 @@ function buildDiscordModelPickerNoticePayload(message: string): { components: Co
|
||||
}
|
||||
|
||||
async function resolveDiscordModelPickerRoute(params: {
|
||||
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
|
||||
interaction:
|
||||
| CommandInteraction
|
||||
| ButtonInteraction
|
||||
| StringSelectMenuInteraction
|
||||
| AutocompleteInteraction;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
accountId: string;
|
||||
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: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
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,
|
||||
createDiscordModelPickerFallbackSelect as createDiscordModelPickerFallbackSelectUi,
|
||||
replyWithDiscordModelPickerProviders,
|
||||
resolveDiscordNativeChoiceContext,
|
||||
shouldOpenDiscordModelPickerFromCommand,
|
||||
type DiscordCommandArgContext,
|
||||
type DiscordModelPickerContext,
|
||||
@ -124,8 +125,11 @@ function resolveDiscordNativeCommandAllowlistAccess(params: {
|
||||
function buildDiscordCommandOptions(params: {
|
||||
command: ChatCommandDefinition;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
resolveChoiceContext?: (
|
||||
interaction: AutocompleteInteraction,
|
||||
) => Promise<{ provider?: string; model?: string } | null>;
|
||||
}): CommandOptions | undefined {
|
||||
const { command, cfg } = params;
|
||||
const { command, cfg, resolveChoiceContext } = params;
|
||||
const args = command.args;
|
||||
if (!args || args.length === 0) {
|
||||
return undefined;
|
||||
@ -158,7 +162,17 @@ function buildDiscordCommandOptions(params: {
|
||||
const focused = interaction.options.getFocused();
|
||||
const focusValue =
|
||||
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
|
||||
? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
|
||||
: choices;
|
||||
@ -297,6 +311,13 @@ export function createDiscordNativeCommand(params: {
|
||||
const commandOptions = buildDiscordCommandOptions({
|
||||
command: commandDefinition,
|
||||
cfg,
|
||||
resolveChoiceContext: async (interaction) =>
|
||||
resolveDiscordNativeChoiceContext({
|
||||
interaction,
|
||||
cfg,
|
||||
accountId,
|
||||
threadBindings,
|
||||
}),
|
||||
});
|
||||
const options = commandOptions
|
||||
? (commandOptions satisfies CommandOptions)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user