mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 18:42:25 +00:00
fix(channels): resolve native /think menu levels via runtime catalog for live-discovered models (#94067)
Merged via squash.
Prepared head SHA: 079347b8b8
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { logVerboseMock } = vi.hoisted(() => ({
|
||||
const { loadModelCatalogMock, logVerboseMock } = vi.hoisted(() => ({
|
||||
loadModelCatalogMock: vi.fn(),
|
||||
logVerboseMock: vi.fn(),
|
||||
}));
|
||||
const { loggerWarnMock } = vi.hoisted(() => ({
|
||||
@@ -32,6 +33,7 @@ vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
|
||||
loadModelCatalog: loadModelCatalogMock,
|
||||
resolveHumanDelayConfig: () => undefined,
|
||||
}));
|
||||
|
||||
@@ -227,6 +229,7 @@ describe("createDiscordNativeCommand option wiring", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
loadModelCatalogMock.mockReset().mockResolvedValue([]);
|
||||
logVerboseMock.mockReset();
|
||||
loggerWarnMock.mockReset();
|
||||
});
|
||||
@@ -257,6 +260,30 @@ describe("createDiscordNativeCommand option wiring", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the provider-startup catalog snapshot for /think autocomplete", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const command = createNativeCommand("think", { cfg });
|
||||
const level = requireOption(command, "level");
|
||||
const autocomplete = requireAutocomplete(level, "think level option did not wire autocomplete");
|
||||
|
||||
await runAutocomplete(autocomplete, {
|
||||
userId: "owner",
|
||||
channelType: ChannelType.DM,
|
||||
channelId: "dm-1",
|
||||
channelName: "dm-1",
|
||||
focusedValue: "",
|
||||
});
|
||||
|
||||
expect(loadModelCatalogMock).toHaveBeenCalledWith({ cacheOnly: true });
|
||||
expect(loadModelCatalogMock).toHaveBeenCalledWith({ config: cfg });
|
||||
});
|
||||
|
||||
it("keeps static choices for non-acp string action arguments", () => {
|
||||
const command = createNativeCommand("config");
|
||||
const action = requireOption(command, "action");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Discord plugin module implements native command.options behavior.
|
||||
import { ApplicationCommandOptionType } from "discord-api-types/v10";
|
||||
import { loadModelCatalog } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
resolveCommandArgChoices,
|
||||
@@ -117,12 +118,17 @@ export function buildDiscordCommandOptions(params: {
|
||||
? await resolveChoiceContext(interaction)
|
||||
: null;
|
||||
const currentCfg = resolveConfig?.() ?? cfg;
|
||||
// Autocomplete cannot defer beyond Discord's three-second deadline.
|
||||
// Cache-only catalog reads never start discovery or filesystem work.
|
||||
const choiceCatalog =
|
||||
command.key === "think" ? await loadModelCatalog({ cacheOnly: true }) : undefined;
|
||||
const choices = resolveCommandArgChoices({
|
||||
command,
|
||||
arg,
|
||||
cfg: currentCfg,
|
||||
provider: context?.provider,
|
||||
model: context?.model,
|
||||
...(choiceCatalog?.length ? { catalog: choiceCatalog } : {}),
|
||||
});
|
||||
const filtered = focusValue
|
||||
? choices.filter((choice) =>
|
||||
@@ -132,6 +138,11 @@ export function buildDiscordCommandOptions(params: {
|
||||
await interaction.respond(
|
||||
filtered.slice(0, 25).map((choice) => ({ name: choice.label, value: choice.value })),
|
||||
);
|
||||
if (command.key === "think" && !choiceCatalog?.length) {
|
||||
// The interaction is acknowledged now, so a failed startup warmup can retry
|
||||
// discovery without risking Discord's response deadline.
|
||||
void loadModelCatalog({ config: currentCfg });
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
const choices =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Discord plugin module implements native command behavior.
|
||||
import { ApplicationCommandOptionType } from "discord-api-types/v10";
|
||||
import { loadModelCatalog } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
@@ -485,12 +486,18 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
threadBindings,
|
||||
})
|
||||
: null;
|
||||
// Native /think choices need live-discovery metadata; empty keeps config fallback.
|
||||
const menuModelCatalog =
|
||||
command.key === "think" && menuNeedsModelContext
|
||||
? await loadModelCatalog({ config: cfg })
|
||||
: undefined;
|
||||
const menu = resolveCommandArgMenu({
|
||||
command,
|
||||
args: commandArgs,
|
||||
cfg,
|
||||
provider: menuModelContext?.provider,
|
||||
model: menuModelContext?.model,
|
||||
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
|
||||
});
|
||||
if (menu) {
|
||||
const menuPayload = buildDiscordCommandArgMenu({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadModelCatalog } from "openclaw/plugin-sdk/agent-runtime";
|
||||
// Discord provider module implements model/runtime integration.
|
||||
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
|
||||
import {
|
||||
@@ -395,6 +396,11 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
let earlyGatewayEmitter = gatewaySupervisor?.emitter;
|
||||
let onEarlyGatewayDebug: ((msg: unknown) => void) | undefined;
|
||||
try {
|
||||
if (nativeEnabled && commandSpecs.some((command) => command.name === "think")) {
|
||||
// Autocomplete cannot defer. Warm opportunistically before interactions begin,
|
||||
// but never let provider discovery block Discord startup.
|
||||
void loadModelCatalog({ config: cfg });
|
||||
}
|
||||
const { commands, components, modals } = createDiscordProviderInteractionSurface({
|
||||
cfg,
|
||||
discordConfig: discordCfg,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Slack plugin module implements slash behavior.
|
||||
import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { loadModelCatalog, resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import {
|
||||
formatCommandArgMenuTitle,
|
||||
@@ -596,11 +596,17 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
sessionKey: menuRoute.sessionKey,
|
||||
})
|
||||
: {};
|
||||
// Native /think choices need live-discovery metadata; empty keeps config fallback.
|
||||
const menuModelCatalog =
|
||||
commandDefinition.key === "think" && menuNeedsModelContext
|
||||
? await loadModelCatalog({ config: cfg })
|
||||
: undefined;
|
||||
const menu = resolveCommandArgMenu({
|
||||
command: commandDefinition,
|
||||
args: commandArgs,
|
||||
cfg,
|
||||
...menuModelContext,
|
||||
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
|
||||
});
|
||||
if (menu) {
|
||||
const commandLabel = commandDefinition.nativeName ?? commandDefinition.key;
|
||||
|
||||
@@ -657,6 +657,68 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves /think menu choices against the runtime catalog for live-discovered models", async () => {
|
||||
const cfg = {
|
||||
agents: { defaults: { models: { "ollama/*": {} } } },
|
||||
} as OpenClawConfig;
|
||||
sessionMocks.loadSessionStore.mockReturnValue({
|
||||
"agent:main:main": {
|
||||
providerOverride: "ollama",
|
||||
modelOverride: "glm-5.2:cloud",
|
||||
modelOverrideSource: "user",
|
||||
updatedAt: 0,
|
||||
},
|
||||
});
|
||||
const runtimeCatalog = [
|
||||
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
|
||||
];
|
||||
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
|
||||
|
||||
const { handler } = registerAndResolveCommandHandler({
|
||||
commandName: "think",
|
||||
cfg,
|
||||
allowFrom: ["*"],
|
||||
});
|
||||
await handler(createTelegramPrivateCommandContext());
|
||||
|
||||
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
||||
([params]) => params.command.key === "think" && params.provider === "ollama",
|
||||
)?.[0];
|
||||
const menuRecord = expectRecordFields(
|
||||
menuCall,
|
||||
{ provider: "ollama", model: "glm-5.2:cloud" },
|
||||
"ollama thinking menu call",
|
||||
);
|
||||
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
|
||||
expect(menuRecord.catalog).toEqual(runtimeCatalog);
|
||||
});
|
||||
|
||||
it("loads the runtime catalog for /think when no session model override is set", async () => {
|
||||
const cfg = {
|
||||
agents: { defaults: { model: "ollama/glm-5.2:cloud", models: { "ollama/*": {} } } },
|
||||
} as OpenClawConfig;
|
||||
sessionMocks.loadSessionStore.mockReturnValue({});
|
||||
const runtimeCatalog = [
|
||||
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
|
||||
];
|
||||
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
|
||||
|
||||
const { handler } = registerAndResolveCommandHandler({
|
||||
commandName: "think",
|
||||
cfg,
|
||||
allowFrom: ["*"],
|
||||
});
|
||||
await handler(createTelegramPrivateCommandContext());
|
||||
|
||||
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
|
||||
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
||||
([params]) => params.command.key === "think",
|
||||
)?.[0];
|
||||
const menuRecord = expectRecordFields(menuCall, {}, "default-model thinking menu call");
|
||||
expect(menuRecord.provider).toBeUndefined();
|
||||
expect(menuRecord.catalog).toEqual(runtimeCatalog);
|
||||
});
|
||||
|
||||
it("inherits the parent session model when building DM thread native argument menus", async () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
sessionMocks.loadSessionStore.mockReturnValue({
|
||||
@@ -855,6 +917,7 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
await handler(createTelegramPrivateCommandContext({ match: "high" }));
|
||||
|
||||
expect(sessionMocks.loadSessionStore).not.toHaveBeenCalled();
|
||||
expect(agentRuntimeMocks.loadModelCatalog).not.toHaveBeenCalled();
|
||||
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1135,12 +1135,18 @@ export const registerTelegramNativeCommands = ({
|
||||
sessionKey: await resolveTargetSessionKey(),
|
||||
})
|
||||
: {};
|
||||
// Native /think choices need live-discovery metadata; empty keeps config fallback.
|
||||
const menuModelCatalog =
|
||||
commandDefinition?.key === "think" && menuNeedsModelContext
|
||||
? await loadModelCatalog({ config: runtimeCfg })
|
||||
: undefined;
|
||||
const menu = commandDefinition
|
||||
? resolveCommandArgMenu({
|
||||
command: commandDefinition,
|
||||
args: commandArgs,
|
||||
cfg: runtimeCfg,
|
||||
...menuModelContext,
|
||||
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
|
||||
})
|
||||
: null;
|
||||
if (menu && commandDefinition) {
|
||||
|
||||
@@ -13,6 +13,7 @@ let findModelInCatalog: typeof import("./model-catalog.js").findModelInCatalog;
|
||||
let loadManifestModelCatalog: typeof import("./model-catalog.js").loadManifestModelCatalog;
|
||||
let loadModelCatalog: typeof import("./model-catalog.js").loadModelCatalog;
|
||||
let modelSupportsInput: typeof import("./model-catalog.js").modelSupportsInput;
|
||||
let resetModelCatalogCache: typeof import("./model-catalog.js").resetModelCatalogCache;
|
||||
let resetModelCatalogCacheForTest: typeof import("./model-catalog.js").resetModelCatalogCacheForTest;
|
||||
let augmentCatalogMock: ReturnType<typeof vi.fn>;
|
||||
let prepareOpenClawModelsJsonSourceMock: ReturnType<typeof vi.fn>;
|
||||
@@ -337,6 +338,7 @@ describe("loadModelCatalog", () => {
|
||||
loadManifestModelCatalog,
|
||||
loadModelCatalog,
|
||||
modelSupportsInput,
|
||||
resetModelCatalogCache,
|
||||
resetModelCatalogCacheForTest,
|
||||
} = await import("./model-catalog.js"));
|
||||
const providerRuntime = await import("../plugins/provider-runtime.runtime.js");
|
||||
@@ -512,6 +514,57 @@ describe("loadModelCatalog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes only a fully loaded process catalog snapshot", async () => {
|
||||
mockAgentDiscoveryModels([
|
||||
{ id: "runtime-reasoner", name: "Runtime Reasoner", provider: "ollama", reasoning: true },
|
||||
]);
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toEqual([]);
|
||||
|
||||
const result = await loadModelCatalog({ config: {} as OpenClawConfig });
|
||||
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toBe(result);
|
||||
resetModelCatalogCache();
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toEqual([]);
|
||||
resetModelCatalogCacheForTest();
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("does not publish a catalog load from an invalidated generation", async () => {
|
||||
let releaseStaleFingerprint:
|
||||
| ((value: { agentDir: string; fingerprint: string; workspaceDir: string }) => void)
|
||||
| undefined;
|
||||
const staleFingerprint = new Promise<{
|
||||
agentDir: string;
|
||||
fingerprint: string;
|
||||
workspaceDir: string;
|
||||
}>((resolve) => {
|
||||
releaseStaleFingerprint = resolve;
|
||||
});
|
||||
buildModelsJsonSourceFingerprintMock.mockReturnValueOnce(staleFingerprint).mockResolvedValue({
|
||||
agentDir: "/tmp/openclaw",
|
||||
fingerprint: "fresh-fingerprint",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
const freshCatalog = [{ id: "fresh", name: "Fresh", provider: "ollama", reasoning: true }];
|
||||
const staleCatalog = [{ id: "stale", name: "Stale", provider: "ollama", reasoning: false }];
|
||||
readCachedAgentModelCatalogMock
|
||||
.mockReturnValueOnce(freshCatalog)
|
||||
.mockReturnValueOnce(staleCatalog);
|
||||
|
||||
const staleLoad = loadModelCatalog({ config: {} as OpenClawConfig });
|
||||
resetModelCatalogCache();
|
||||
await expect(loadModelCatalog({ config: {} as OpenClawConfig })).resolves.toBe(freshCatalog);
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toBe(freshCatalog);
|
||||
|
||||
releaseStaleFingerprint?.({
|
||||
agentDir: "/tmp/openclaw",
|
||||
fingerprint: "stale-fingerprint",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
await expect(staleLoad).resolves.toBe(staleCatalog);
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toBe(freshCatalog);
|
||||
});
|
||||
|
||||
it("preserves runtime model params in the internal catalog", async () => {
|
||||
mockAgentDiscoveryModels([
|
||||
{
|
||||
@@ -727,6 +780,7 @@ describe("loadModelCatalog", () => {
|
||||
|
||||
const result = await loadModelCatalog({ config: {} as OpenClawConfig });
|
||||
expect(result).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]);
|
||||
await expect(loadModelCatalog({ cacheOnly: true })).resolves.toEqual([]);
|
||||
} finally {
|
||||
setLoggerOverride(null);
|
||||
resetLogger();
|
||||
|
||||
@@ -78,6 +78,9 @@ type DiscoveredModel = {
|
||||
type AgentDiscoveryModule = typeof import("./agent-model-discovery.js");
|
||||
|
||||
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
||||
let loadedModelCatalogSnapshot: ModelCatalogEntry[] | undefined;
|
||||
let loadedModelCatalogGeneration = -1;
|
||||
let modelCatalogGeneration = 0;
|
||||
let hasLoggedModelCatalogError = false;
|
||||
let hasLoggedReadOnlyStaticCatalogError = false;
|
||||
type ManifestModelCatalogCacheEntry = {
|
||||
@@ -127,6 +130,7 @@ function loadProviderApiKeyResolver() {
|
||||
|
||||
export function resetModelCatalogCache() {
|
||||
modelCatalogPromise = null;
|
||||
modelCatalogGeneration += 1;
|
||||
manifestModelCatalogCache = new WeakMap();
|
||||
hasLoggedModelCatalogError = false;
|
||||
hasLoggedReadOnlyStaticCatalogError = false;
|
||||
@@ -134,6 +138,8 @@ export function resetModelCatalogCache() {
|
||||
|
||||
export function resetModelCatalogCacheForTest() {
|
||||
resetModelCatalogCache();
|
||||
loadedModelCatalogSnapshot = undefined;
|
||||
loadedModelCatalogGeneration = -1;
|
||||
importAgentDiscovery = defaultImportAgentDiscovery;
|
||||
}
|
||||
|
||||
@@ -542,9 +548,15 @@ function loadReadOnlyStaticModelCatalog(params?: {
|
||||
export async function loadModelCatalog(params?: {
|
||||
config?: OpenClawConfig;
|
||||
useCache?: boolean;
|
||||
cacheOnly?: boolean;
|
||||
readOnly?: boolean;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Promise<ModelCatalogEntry[]> {
|
||||
if (params?.cacheOnly === true) {
|
||||
return loadedModelCatalogGeneration === modelCatalogGeneration
|
||||
? (loadedModelCatalogSnapshot ?? [])
|
||||
: [];
|
||||
}
|
||||
const readOnly = params?.readOnly === true;
|
||||
if (readOnly) {
|
||||
try {
|
||||
@@ -557,6 +569,7 @@ export async function loadModelCatalog(params?: {
|
||||
}
|
||||
if (!readOnly && params?.useCache === false) {
|
||||
modelCatalogPromise = null;
|
||||
modelCatalogGeneration += 1;
|
||||
}
|
||||
const useSharedCache = !readOnly && !params?.metadataSnapshot;
|
||||
if (useSharedCache && modelCatalogPromise) {
|
||||
@@ -811,8 +824,20 @@ export async function loadModelCatalog(params?: {
|
||||
return loadCatalog();
|
||||
}
|
||||
|
||||
modelCatalogPromise = loadCatalog();
|
||||
return modelCatalogPromise;
|
||||
const loadGeneration = modelCatalogGeneration;
|
||||
const publishedPromise = loadCatalog().then((catalog) => {
|
||||
if (
|
||||
catalog.length > 0 &&
|
||||
modelCatalogGeneration === loadGeneration &&
|
||||
modelCatalogPromise === publishedPromise
|
||||
) {
|
||||
loadedModelCatalogSnapshot = catalog;
|
||||
loadedModelCatalogGeneration = loadGeneration;
|
||||
}
|
||||
return catalog;
|
||||
});
|
||||
modelCatalogPromise = publishedPromise;
|
||||
return publishedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -740,24 +740,37 @@ describe("commands registry args", () => {
|
||||
expect(seenChoice.catalogLength).toBe(0);
|
||||
});
|
||||
|
||||
it("uses configured model catalog reasoning for /think arg menus", () => {
|
||||
installOllamaThinkingProvider();
|
||||
const command = requireNativeCommand("think");
|
||||
|
||||
const menu = requireCommandArgMenu({
|
||||
command,
|
||||
args: undefined,
|
||||
it.each([
|
||||
{
|
||||
source: "configured",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
models: [{ id: "glm-5.1:cloud", name: "GLM 5.1 Cloud", reasoning: true }],
|
||||
models: [{ id: "glm-5.2:cloud", name: "GLM 5.2 Cloud", reasoning: true }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
catalog: undefined,
|
||||
},
|
||||
{
|
||||
source: "runtime",
|
||||
cfg: { agents: { defaults: { models: { "ollama/*": {} } } } },
|
||||
catalog: [
|
||||
{ provider: "ollama", id: "glm-5.2:cloud", name: "GLM 5.2 Cloud", reasoning: true },
|
||||
],
|
||||
},
|
||||
])("uses $source model catalog reasoning for /think arg menus", ({ cfg, catalog }) => {
|
||||
installOllamaThinkingProvider();
|
||||
const command = requireNativeCommand("think");
|
||||
const menu = requireCommandArgMenu({
|
||||
command,
|
||||
args: undefined,
|
||||
cfg: cfg as never,
|
||||
provider: "ollama",
|
||||
model: "glm-5.1:cloud",
|
||||
model: "glm-5.2:cloud",
|
||||
catalog,
|
||||
});
|
||||
|
||||
expect(menu.arg.name).toBe("level");
|
||||
|
||||
@@ -57,6 +57,7 @@ const hoisted = vi.hoisted(() => ({
|
||||
markRestartAbortedMainSessions: vi.fn(async (_params: unknown) => ({ marked: 1, skipped: 0 })),
|
||||
runtimeConfig: { value: { session: { store: "/tmp/active-sessions.json" } } as OpenClawConfig },
|
||||
reloadEvents: [] as string[],
|
||||
loadModelCatalog: vi.fn(async (_params: { config: OpenClawConfig }) => []),
|
||||
resetModelCatalogCache: vi.fn(() => {}),
|
||||
refreshContextWindowCache: vi.fn(async (_cfg: OpenClawConfig) => {}),
|
||||
clearCurrentProviderAuthState: vi.fn(() => {}),
|
||||
@@ -118,6 +119,10 @@ vi.mock("../config/config.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => ({
|
||||
loadModelCatalog: (params: { config: OpenClawConfig }) => {
|
||||
hoisted.reloadEvents.push("load-model-catalog");
|
||||
return hoisted.loadModelCatalog(params);
|
||||
},
|
||||
resetModelCatalogCache: () => {
|
||||
hoisted.reloadEvents.push("reset-model-catalog");
|
||||
hoisted.resetModelCatalogCache();
|
||||
@@ -198,6 +203,7 @@ afterEach(() => {
|
||||
hoisted.markRestartAbortedMainSessions.mockClear();
|
||||
hoisted.runtimeConfig.value = { session: { store: "/tmp/active-sessions.json" } };
|
||||
hoisted.reloadEvents.length = 0;
|
||||
hoisted.loadModelCatalog.mockClear();
|
||||
hoisted.resetModelCatalogCache.mockClear();
|
||||
hoisted.refreshContextWindowCache.mockClear();
|
||||
hoisted.clearCurrentProviderAuthState.mockClear();
|
||||
@@ -271,9 +277,11 @@ describe("gateway hot reload model state", () => {
|
||||
"reset-model-catalog",
|
||||
"clear-provider-auth",
|
||||
"refresh-context-window",
|
||||
"load-model-catalog",
|
||||
"warm-provider-auth",
|
||||
]);
|
||||
expect(hoisted.refreshContextWindowCache).toHaveBeenCalledWith(nextConfig);
|
||||
expect(hoisted.loadModelCatalog).toHaveBeenCalledWith({ config: nextConfig });
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledWith(nextConfig);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
listActiveEmbeddedRunSessionIds,
|
||||
listActiveEmbeddedRunSessionKeys,
|
||||
} from "../agents/embedded-agent-runner/run-state.js";
|
||||
import { resetModelCatalogCache } from "../agents/model-catalog.js";
|
||||
import { loadModelCatalog, resetModelCatalogCache } from "../agents/model-catalog.js";
|
||||
import {
|
||||
clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
@@ -524,6 +524,8 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
|
||||
|
||||
if (shouldRefreshContextWindowCache(plan)) {
|
||||
await refreshContextWindowCache(nextConfig);
|
||||
// Provider discovery is best-effort; a slow hook must not hold hot reload open.
|
||||
void loadModelCatalog({ config: nextConfig });
|
||||
}
|
||||
void warmCurrentProviderAuthStateOffMainThread(nextConfig).catch((err: unknown) => {
|
||||
params.logReload.warn(`provider auth state rewarm failed: ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user