mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 04:20:44 +00:00
fix(telegram): allow fallback models in /model validation (#40105)
Merged via squash.
Prepared head SHA: de07585e03
Co-authored-by: avirweb <257412074+avirweb@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/main-session routing: keep TUI and other `mode:UI` main-session sends on the internal surface when `deliver` is enabled, so replies no longer inherit the session's persisted Telegram/WhatsApp route. (#43918) Thanks @obviyus.
|
||||
- Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.
|
||||
- Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.
|
||||
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
@@ -234,6 +235,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/skills tables: keep terminal table borders aligned for wide graphemes, use full reported terminal width, and switch a few ambiguous skill icons to Terminal-safe emoji so `openclaw skills` renders more consistently in Terminal.app and iTerm. Thanks @vincentkoc.
|
||||
- Memory/Gemini: normalize returned Gemini embeddings across direct query, direct batch, and async batch paths so memory search uses consistent vector handling for Gemini too. (#43409) Thanks @gumadeiras.
|
||||
- Agents/failover: recognize additional serialized network errno strings plus `EHOSTDOWN` and `EPIPE` structured codes so transient transport failures trigger timeout failover more reliably. (#42830) Thanks @jnMetaCode.
|
||||
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
|
||||
|
||||
## 2026.3.7
|
||||
|
||||
|
||||
@@ -322,6 +322,98 @@ describe("model-selection", () => {
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes fallback models in allowed set", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4o": {},
|
||||
},
|
||||
model: {
|
||||
primary: "openai/gpt-4o",
|
||||
fallbacks: ["anthropic/claude-sonnet-4-6", "google/gemini-3-pro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = buildAllowedModelSet({
|
||||
cfg,
|
||||
catalog: [],
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o",
|
||||
});
|
||||
|
||||
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
|
||||
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
|
||||
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true);
|
||||
expect(result.allowAny).toBe(false);
|
||||
});
|
||||
|
||||
it("handles empty fallbacks gracefully", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4o": {},
|
||||
},
|
||||
model: {
|
||||
primary: "openai/gpt-4o",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = buildAllowedModelSet({
|
||||
cfg,
|
||||
catalog: [],
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o",
|
||||
});
|
||||
|
||||
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
|
||||
expect(result.allowAny).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers per-agent fallback overrides when agentId is provided", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-4o": {},
|
||||
},
|
||||
model: {
|
||||
primary: "openai/gpt-4o",
|
||||
fallbacks: ["google/gemini-3-pro"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "coder",
|
||||
model: {
|
||||
primary: "openai/gpt-4o",
|
||||
fallbacks: ["anthropic/claude-sonnet-4-6"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = buildAllowedModelSet({
|
||||
cfg,
|
||||
catalog: [],
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o",
|
||||
agentId: "coder",
|
||||
});
|
||||
|
||||
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
|
||||
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
|
||||
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(false);
|
||||
expect(result.allowAny).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAllowedModelRef", () => {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue, toAgentModelListLike } from "../config/model-input.js";
|
||||
import {
|
||||
resolveAgentModelFallbackValues,
|
||||
resolveAgentModelPrimaryValue,
|
||||
toAgentModelListLike,
|
||||
} from "../config/model-input.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
} from "./agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||
import { splitTrailingAuthProfile } from "./model-ref-profile.js";
|
||||
@@ -382,6 +390,16 @@ export function resolveDefaultModelForAgent(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAllowedFallbacks(params: { cfg: OpenClawConfig; agentId?: string }): string[] {
|
||||
if (params.agentId) {
|
||||
const override = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
|
||||
if (override !== undefined) {
|
||||
return override;
|
||||
}
|
||||
}
|
||||
return resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
|
||||
}
|
||||
|
||||
export function resolveSubagentConfiguredModelSelection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
@@ -419,6 +437,7 @@ export function buildAllowedModelSet(params: {
|
||||
catalog: ModelCatalogEntry[];
|
||||
defaultProvider: string;
|
||||
defaultModel?: string;
|
||||
agentId?: string;
|
||||
}): {
|
||||
allowAny: boolean;
|
||||
allowedCatalog: ModelCatalogEntry[];
|
||||
@@ -469,6 +488,25 @@ export function buildAllowedModelSet(params: {
|
||||
}
|
||||
}
|
||||
|
||||
for (const fallback of resolveAllowedFallbacks({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
})) {
|
||||
const parsed = parseModelRef(String(fallback), params.defaultProvider);
|
||||
if (parsed) {
|
||||
const key = modelKey(parsed.provider, parsed.model);
|
||||
allowedKeys.add(key);
|
||||
|
||||
if (!catalogKeys.has(key) && !syntheticCatalogEntries.has(key)) {
|
||||
syntheticCatalogEntries.set(key, {
|
||||
id: parsed.model,
|
||||
name: parsed.model,
|
||||
provider: parsed.provider,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultKey) {
|
||||
allowedKeys.add(defaultKey);
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ async function resolveModelOverride(params: {
|
||||
catalog,
|
||||
defaultProvider: currentProvider,
|
||||
defaultModel: currentModel,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function buildModelsProviderData(
|
||||
catalog,
|
||||
defaultProvider: resolvedDefault.provider,
|
||||
defaultModel: resolvedDefault.model,
|
||||
agentId,
|
||||
});
|
||||
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
|
||||
@@ -373,6 +373,7 @@ export async function resolveReplyDirectives(params: {
|
||||
|
||||
const modelState = await createModelSelectionState({
|
||||
cfg,
|
||||
agentId,
|
||||
agentCfg,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function getReplyFromConfig(
|
||||
|
||||
await applyResetModelOverride({
|
||||
cfg,
|
||||
agentId,
|
||||
resetTriggered,
|
||||
bodyStripped,
|
||||
sessionCtx,
|
||||
|
||||
@@ -263,6 +263,7 @@ function scoreFuzzyMatch(params: {
|
||||
|
||||
export async function createModelSelectionState(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
agentCfg: NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]> | undefined;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
@@ -315,6 +316,7 @@ export async function createModelSelectionState(params: {
|
||||
catalog: modelCatalog,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
allowedModelCatalog = allowed.allowedCatalog;
|
||||
allowedModelKeys = allowed.allowedKeys;
|
||||
|
||||
@@ -87,6 +87,7 @@ function applySelectionToSession(params: {
|
||||
|
||||
export async function applyResetModelOverride(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
resetTriggered: boolean;
|
||||
bodyStripped?: string;
|
||||
sessionCtx: TemplateContext;
|
||||
@@ -118,6 +119,7 @@ export async function applyResetModelOverride(params: {
|
||||
catalog,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const allowedModelKeys = allowed.allowedKeys;
|
||||
if (allowedModelKeys.size === 0) {
|
||||
|
||||
@@ -950,6 +950,7 @@ async function agentCommandInternal(
|
||||
catalog: modelCatalog,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
allowedModelKeys = allowed.allowedKeys;
|
||||
allowedModelCatalog = allowed.allowedCatalog;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Message, ReactionTypeEmoji } from "@grammyjs/types";
|
||||
import { resolveAgentDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||
import {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import type { DmPolicy } from "../config/types.base.js";
|
||||
import type {
|
||||
@@ -33,6 +35,7 @@ import { MediaFetchError } from "../media/fetch.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
import {
|
||||
isSenderAllowed,
|
||||
@@ -300,6 +303,7 @@ export const registerTelegramHandlers = ({
|
||||
}): {
|
||||
agentId: string;
|
||||
sessionEntry: ReturnType<typeof loadSessionStore>[string] | undefined;
|
||||
sessionKey: string;
|
||||
model?: string;
|
||||
} => {
|
||||
const resolvedThreadId =
|
||||
@@ -339,6 +343,7 @@ export const registerTelegramHandlers = ({
|
||||
return {
|
||||
agentId: route.agentId,
|
||||
sessionEntry: entry,
|
||||
sessionKey,
|
||||
model: storedOverride.provider
|
||||
? `${storedOverride.provider}/${storedOverride.model}`
|
||||
: storedOverride.model,
|
||||
@@ -350,6 +355,7 @@ export const registerTelegramHandlers = ({
|
||||
return {
|
||||
agentId: route.agentId,
|
||||
sessionEntry: entry,
|
||||
sessionKey,
|
||||
model: `${provider}/${model}`,
|
||||
};
|
||||
}
|
||||
@@ -357,6 +363,7 @@ export const registerTelegramHandlers = ({
|
||||
return {
|
||||
agentId: route.agentId,
|
||||
sessionEntry: entry,
|
||||
sessionKey,
|
||||
model: typeof modelCfg === "string" ? modelCfg : modelCfg?.primary,
|
||||
};
|
||||
};
|
||||
@@ -1374,16 +1381,56 @@ export const registerTelegramHandlers = ({
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Process model selection as a synthetic message with /model command
|
||||
const syntheticMessage = buildSyntheticTextMessage({
|
||||
base: callbackMessage,
|
||||
from: callback.from,
|
||||
text: `/model ${selection.provider}/${selection.model}`,
|
||||
});
|
||||
await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, {
|
||||
forceWasMentioned: true,
|
||||
messageIdOverride: callback.id,
|
||||
});
|
||||
|
||||
const modelSet = byProvider.get(selection.provider);
|
||||
if (!modelSet?.has(selection.model)) {
|
||||
await editMessageWithButtons(
|
||||
`❌ Model "${selection.provider}/${selection.model}" is not allowed.`,
|
||||
[],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Directly set model override in session
|
||||
try {
|
||||
// Get session store path
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
agentId: sessionState.agentId,
|
||||
});
|
||||
|
||||
const resolvedDefault = resolveDefaultModelForAgent({
|
||||
cfg,
|
||||
agentId: sessionState.agentId,
|
||||
});
|
||||
const isDefaultSelection =
|
||||
selection.provider === resolvedDefault.provider &&
|
||||
selection.model === resolvedDefault.model;
|
||||
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const sessionKey = sessionState.sessionKey;
|
||||
const entry = store[sessionKey] ?? {};
|
||||
store[sessionKey] = entry;
|
||||
applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: selection.provider,
|
||||
model: selection.model,
|
||||
isDefault: isDefaultSelection,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Update message to show success with visual feedback
|
||||
const actionText = isDefaultSelection
|
||||
? "reset to default"
|
||||
: `changed to **${selection.provider}/${selection.model}**`;
|
||||
await editMessageWithButtons(
|
||||
`✅ Model ${actionText}\n\nThis model will be used for your next message.`,
|
||||
[], // Empty buttons = remove inline keyboard
|
||||
);
|
||||
} catch (err) {
|
||||
await editMessageWithButtons(`❌ Failed to change model: ${String(err)}`, []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
|
||||
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
import { loadSessionStore } from "../config/sessions.js";
|
||||
import { normalizeTelegramCommandName } from "../config/telegram-custom-commands.js";
|
||||
import {
|
||||
answerCallbackQuerySpy,
|
||||
@@ -531,49 +533,127 @@ describe("createTelegramBot", () => {
|
||||
it("routes compact model callbacks by inferring provider", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
editMessageTextSpy.mockClear();
|
||||
|
||||
const modelId = "us.anthropic.claude-3-5-sonnet-20240620-v1:0";
|
||||
const storePath = `/tmp/openclaw-telegram-model-compact-${process.pid}-${Date.now()}.json`;
|
||||
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: `bedrock/${modelId}`,
|
||||
await rm(storePath, { force: true });
|
||||
try {
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: `bedrock/${modelId}`,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
});
|
||||
const callbackHandler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "callback_query",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-model-compact-1",
|
||||
data: `mdl_sel/${modelId}`,
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-model-compact-1",
|
||||
data: `mdl_sel/${modelId}`,
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 14,
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
|
||||
expect(editMessageTextSpy.mock.calls[0]?.[2]).toContain("✅ Model reset to default");
|
||||
|
||||
const entry = Object.values(loadSessionStore(storePath, { skipCache: true }))[0];
|
||||
expect(entry?.providerOverride).toBeUndefined();
|
||||
expect(entry?.modelOverride).toBeUndefined();
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-compact-1");
|
||||
} finally {
|
||||
await rm(storePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resets overrides when selecting the configured default model", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
editMessageTextSpy.mockClear();
|
||||
|
||||
const storePath = `/tmp/openclaw-telegram-model-default-${process.pid}-${Date.now()}.json`;
|
||||
|
||||
await rm(storePath, { force: true });
|
||||
try {
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "claude-opus-4-6",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: storePath,
|
||||
},
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
});
|
||||
const callbackHandler = onSpy.mock.calls.find(
|
||||
(call) => call[0] === "callback_query",
|
||||
)?.[1] as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
expect(callbackHandler).toBeDefined();
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0]?.[0];
|
||||
expect(payload?.Body).toContain(`/model amazon-bedrock/${modelId}`);
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-compact-1");
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-model-default-1",
|
||||
data: "mdl_sel_anthropic/claude-opus-4-6",
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 16,
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
|
||||
expect(editMessageTextSpy.mock.calls[0]?.[2]).toContain("✅ Model reset to default");
|
||||
|
||||
const entry = Object.values(loadSessionStore(storePath, { skipCache: true }))[0];
|
||||
expect(entry?.providerOverride).toBeUndefined();
|
||||
expect(entry?.modelOverride).toBeUndefined();
|
||||
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-model-default-1");
|
||||
} finally {
|
||||
await rm(storePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects ambiguous compact model callbacks and returns provider list", async () => {
|
||||
|
||||
Reference in New Issue
Block a user