fix(discord): defer model picker interactions

This commit is contained in:
Peter Steinberger
2026-05-10 07:40:26 +01:00
parent 59fd3e6481
commit 10db5a67aa
3 changed files with 53 additions and 8 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Telegram: keep no-response DM turns quiet instead of rewriting them into visible silent-reply chatter. Fixes #78188. (#78228) Thanks @Beandon13.
- Telegram: handle managed select button callbacks before the raw callback fallback while preserving delimiter-containing option values such as `env|prod`. (#79816) Thanks @moeedahmed.
- OpenAI-compatible models: handle JSON chat-completion bodies returned to streaming requests, preserving reasoning fields and visible text instead of completing an empty agent turn. Fixes #77870.
- Discord/models: defer model picker component interactions before loading route, model, and preference data, preventing "This interaction failed" timeouts under gateway load. Fixes #77283. Thanks @colin-chang.
- xAI: expose `/think low|medium|high` for reasoning-capable Grok models and keep `reasoning.effort` on native Responses payloads while preserving off-only behavior for non-reasoning routes. Fixes #79210. Thanks @colinmcintosh.
- CLI/media: let explicit image description model refs use bundled static provider catalogs and generic model-backed image hooks, so `openclaw infer image describe --model zai/glm-4.6v` works like direct model runs and Anthropic auth probes avoid stale Claude 3 Haiku catalog entries.
- Models/Anthropic: add `anthropic/claude-haiku-4-5` to Anthropic API-key agent allowlist defaults when an Anthropic default model is configured, so cron model overrides can select the current Haiku alias. Fixes #78000.

View File

@@ -147,6 +147,17 @@ export async function handleDiscordModelPickerInteraction(params: {
return;
}
let deferredUpdate = interaction.acknowledged;
if (!deferredUpdate) {
const deferred = await params.safeInteractionCall("model picker defer", () =>
interaction.acknowledge(),
);
if (deferred === null) {
return;
}
deferredUpdate = true;
}
const route = await resolveDiscordModelPickerRoute({
interaction,
cfg: ctx.cfg,
@@ -175,7 +186,9 @@ export async function handleDiscordModelPickerInteraction(params: {
limit: 5,
});
const updatePicker = async (payload: MessagePayload) =>
await params.safeInteractionCall("model picker update", () => interaction.update(payload));
await params.safeInteractionCall("model picker update", () =>
deferredUpdate ? interaction.editReply(payload) : interaction.update(payload),
);
const showNotice = async (message: string) =>
await updatePicker(buildDiscordModelPickerNoticePayload(message));

View File

@@ -44,7 +44,9 @@ type MockInteraction = {
reply: ReturnType<typeof vi.fn>;
followUp: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
editReply: ReturnType<typeof vi.fn>;
acknowledge: ReturnType<typeof vi.fn>;
acknowledged: boolean;
client: object;
};
@@ -96,7 +98,7 @@ function createModelPickerContext(): ModelPickerContext {
function createInteraction(params?: { userId?: string; values?: string[] }): MockInteraction {
const userId = params?.userId ?? "owner";
return {
const interaction = {
user: {
id: userId,
username: "tester",
@@ -115,9 +117,16 @@ function createInteraction(params?: { userId?: string; values?: string[] }): Moc
reply: vi.fn().mockResolvedValue({ ok: true }),
followUp: vi.fn().mockResolvedValue({ ok: true }),
update: vi.fn().mockResolvedValue({ ok: true }),
acknowledge: vi.fn().mockResolvedValue({ ok: true }),
editReply: vi.fn().mockResolvedValue({ ok: true }),
acknowledge: vi.fn(),
acknowledged: false,
client: {},
};
interaction.acknowledge.mockImplementation(async () => {
interaction.acknowledged = true;
return { ok: true };
});
return interaction;
}
function createDefaultModelPickerData(): ModelsProviderData {
@@ -323,6 +332,28 @@ describe("Discord model picker interactions", () => {
expect(loadSpy).not.toHaveBeenCalled();
});
it("defers owner picker interactions before loading model data", async () => {
const context = createModelPickerContext();
const pickerData = createDefaultModelPickerData();
const loadSpy = vi
.spyOn(modelPickerModule, "loadDiscordModelPickerData")
.mockImplementation(async () => {
expect(interaction.acknowledge).toHaveBeenCalledTimes(1);
return pickerData;
});
const select = createModelPickerFallbackSelect(context);
const interaction = createInteraction({ userId: "owner", values: ["gpt-4o"] });
await select.run(
interaction as unknown as PickerSelectInteraction,
createModelsViewSelectData(),
);
expect(loadSpy).toHaveBeenCalledTimes(1);
expect(interaction.editReply).toHaveBeenCalledTimes(1);
expect(interaction.update).not.toHaveBeenCalled();
});
it("requires submit click before routing selected model through /model pipeline", async () => {
const context = createModelPickerContext();
const pickerData = createDefaultModelPickerData();
@@ -338,7 +369,7 @@ describe("Discord model picker interactions", () => {
dispatchCommandInteraction: dispatchSpy,
});
expect(selectInteraction.update).toHaveBeenCalledTimes(1);
expect(selectInteraction.editReply).toHaveBeenCalledTimes(1);
expect(dispatchSpy).not.toHaveBeenCalled();
const submitInteraction = await runSubmitButton({
@@ -347,7 +378,7 @@ describe("Discord model picker interactions", () => {
dispatchCommandInteraction: dispatchSpy,
});
expect(submitInteraction.update).toHaveBeenCalledTimes(1);
expect(submitInteraction.editReply).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectDispatchedModelSelection({
dispatchSpy,
@@ -547,8 +578,8 @@ describe("Discord model picker interactions", () => {
await button.run(interaction as unknown as PickerButtonInteraction, data);
expect(interaction.update).toHaveBeenCalledTimes(1);
const updatePayload = interaction.update.mock.calls[0]?.[0];
expect(interaction.editReply).toHaveBeenCalledTimes(1);
const updatePayload = interaction.editReply.mock.calls[0]?.[0];
if (!updatePayload) {
throw new Error("recents button did not emit an update payload");
}
@@ -585,7 +616,7 @@ describe("Discord model picker interactions", () => {
dispatchCommandInteraction: dispatchSpy,
});
expect(submitInteraction.update).toHaveBeenCalledTimes(1);
expect(submitInteraction.editReply).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectDispatchedModelSelection({ dispatchSpy, model: "openai/gpt-4o" });
});