mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(agents): prevent model runtime startup timeout (#116039)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
|
||||
const manifestMocks = vi.hoisted(() => ({
|
||||
getCurrentPluginMetadataSnapshot: vi.fn(),
|
||||
@@ -176,6 +177,35 @@ describe("bundled static model catalog snapshot cache", () => {
|
||||
expect(manifestMocks.loadPluginManifest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pins lifecycle lookups to the supplied plugin generation", () => {
|
||||
const cfg = {};
|
||||
const capturedPlugin = createMistralManifestPlugin();
|
||||
const capturedSnapshot = {
|
||||
plugins: [capturedPlugin],
|
||||
manifestRegistry: { plugins: [capturedPlugin] },
|
||||
} as unknown as PluginMetadataSnapshot;
|
||||
const resolveModel = createBundledStaticCatalogModelResolver({
|
||||
cfg,
|
||||
metadataSnapshot: capturedSnapshot,
|
||||
});
|
||||
|
||||
const replacementPlugin = createMistralManifestPlugin();
|
||||
replacementPlugin.modelCatalog.providers.mistral.models =
|
||||
replacementPlugin.modelCatalog.providers.mistral.models.map((model) => ({
|
||||
...model,
|
||||
id: "mistral-medium-next",
|
||||
name: "Mistral Medium Next",
|
||||
}));
|
||||
setCurrentManifestPlugins([replacementPlugin]);
|
||||
|
||||
expect(resolveModel({ provider: "mistral", modelId: "mistral-medium-3-5" })?.id).toBe(
|
||||
"mistral-medium-3-5",
|
||||
);
|
||||
expect(resolveModel({ provider: "mistral", modelId: "mistral-medium-next" })).toBeUndefined();
|
||||
expect(manifestMocks.listOpenClawPluginManifestMetadata).not.toHaveBeenCalled();
|
||||
expect(manifestMocks.loadPluginManifest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the matching configured workspace snapshot", () => {
|
||||
const cfg = {};
|
||||
const workspaceDir = "/configured-workspace";
|
||||
|
||||
@@ -131,6 +131,7 @@ type StaticCatalogPlugin = Parameters<
|
||||
type BundledStaticCatalogParams = {
|
||||
cfg?: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
@@ -150,6 +151,11 @@ const defaultBundledStaticCatalogConfig: OpenClawConfig = {};
|
||||
function resolveBundledStaticCatalogMetadataSnapshot(
|
||||
params: BundledStaticCatalogParams,
|
||||
): PluginMetadataSnapshot | undefined {
|
||||
// Lifecycle callers pin the catalog to the plugin generation they are publishing.
|
||||
// Rediscovery here can mix generations and repeat manifest work for every model lookup.
|
||||
if (params.metadataSnapshot) {
|
||||
return params.metadataSnapshot;
|
||||
}
|
||||
if (params.env !== process.env) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -280,11 +286,13 @@ export function createBundledStaticCatalogModelResolver(params?: {
|
||||
cfg?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
includeRuntimeDiscovery?: boolean;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
}): (lookup: BundledStaticCatalogLookup) => ProviderRuntimeModel | undefined {
|
||||
const catalogParams = {
|
||||
cfg: params?.cfg,
|
||||
env: params?.env ?? process.env,
|
||||
...(params?.metadataSnapshot ? { metadataSnapshot: params.metadataSnapshot } : {}),
|
||||
workspaceDir: params?.workspaceDir,
|
||||
};
|
||||
let standaloneState: BundledStaticCatalogState | undefined;
|
||||
|
||||
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => ({
|
||||
})),
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
loadStaticCatalog: vi.fn<LoadStaticCatalog>(async () => []),
|
||||
resolveStaticCatalogModel: vi.fn(() => undefined),
|
||||
configuredAgentIds: [] as string[],
|
||||
warn: vi.fn(),
|
||||
mutationListener: undefined as
|
||||
@@ -78,7 +79,7 @@ vi.mock("./runtime-plugins.js", () => ({
|
||||
vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({
|
||||
loadBundledProviderStaticCatalogContextModels: (...args: Parameters<LoadStaticCatalog>) =>
|
||||
mocks.loadStaticCatalog(...args),
|
||||
resolveBundledStaticCatalogModel: () => undefined,
|
||||
createBundledStaticCatalogModelResolver: () => mocks.resolveStaticCatalogModel,
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
type InlineModelEntry,
|
||||
} from "./embedded-agent-runner/model.inline-provider.js";
|
||||
import {
|
||||
createBundledStaticCatalogModelResolver,
|
||||
loadBundledProviderStaticCatalogContextModels,
|
||||
resolveBundledStaticCatalogModel,
|
||||
} from "./embedded-agent-runner/model.static-catalog.js";
|
||||
import { staticModelIdMatches } from "./embedded-agent-runner/model.static-id.js";
|
||||
import { buildPreparedModelCatalogSnapshot, type ModelCatalogEntry } from "./model-catalog.js";
|
||||
@@ -318,11 +318,19 @@ function collectPreparedModelRuntimeProviderIds(
|
||||
function prepareConfiguredRuntimeModels(params: {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
metadataSnapshot: PluginMetadataSnapshot;
|
||||
providerStaticModels: readonly ProviderRuntimeModel[];
|
||||
workspaceDir?: string;
|
||||
}): PreparedConfiguredRuntimeModel[] {
|
||||
const prepared: PreparedConfiguredRuntimeModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
const resolveStaticCatalogModel = createBundledStaticCatalogModelResolver({
|
||||
cfg: params.config,
|
||||
env: params.env,
|
||||
includeRuntimeDiscovery: true,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
for (const { value } of collectConfiguredModelRefs(params.config)) {
|
||||
const separator = value.indexOf("/");
|
||||
if (separator <= 0 || separator >= value.length - 1) {
|
||||
@@ -341,14 +349,7 @@ function prepareConfiguredRuntimeModels(params: {
|
||||
// Match request-time fallback precedence exactly: manifest/runtime-discovery rows win,
|
||||
// and the provider-static catalog fills only models absent from that surface.
|
||||
const model =
|
||||
resolveBundledStaticCatalogModel({
|
||||
provider,
|
||||
modelId,
|
||||
cfg: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
}) ??
|
||||
resolveStaticCatalogModel({ provider, modelId }) ??
|
||||
params.providerStaticModels.find((candidate) =>
|
||||
staticModelIdMatches({
|
||||
candidateId: candidate.id,
|
||||
@@ -541,6 +542,7 @@ async function buildSnapshot(
|
||||
const configuredRuntimeModels = prepareConfiguredRuntimeModels({
|
||||
config: input.config,
|
||||
env,
|
||||
metadataSnapshot: pluginMetadataSnapshot,
|
||||
providerStaticModels,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ const mocks = vi.hoisted(() => {
|
||||
buildPreparedModelCatalogSnapshot: vi.fn(async () => ({ entries: [], routeVariants: [] })),
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
loadStaticCatalog: vi.fn(async () => []),
|
||||
resolveStaticCatalogModel: vi.fn(() => undefined),
|
||||
mutationListener: undefined as
|
||||
| ((event: { agentDir?: string; affectsInheritedStores: boolean }) => void)
|
||||
| undefined,
|
||||
@@ -80,7 +81,7 @@ vi.mock("./runtime-plugins.js", () => ({
|
||||
|
||||
vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({
|
||||
loadBundledProviderStaticCatalogContextModels: mocks.loadStaticCatalog,
|
||||
resolveBundledStaticCatalogModel: () => undefined,
|
||||
createBundledStaticCatalogModelResolver: () => mocks.resolveStaticCatalogModel,
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
|
||||
@@ -2,8 +2,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type LoadStaticCatalog =
|
||||
typeof import("./embedded-agent-runner/model.static-catalog.js").loadBundledProviderStaticCatalogContextModels;
|
||||
type ResolveStaticCatalog =
|
||||
typeof import("./embedded-agent-runner/model.static-catalog.js").resolveBundledStaticCatalogModel;
|
||||
type CreateStaticCatalogResolver =
|
||||
typeof import("./embedded-agent-runner/model.static-catalog.js").createBundledStaticCatalogModelResolver;
|
||||
type StaticCatalogResolver = ReturnType<CreateStaticCatalogResolver>;
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
authStorage: { getAll: vi.fn(() => ({ custom: { type: "api_key", key: "test-key" } })) },
|
||||
@@ -23,7 +24,8 @@ const mocks = vi.hoisted(() => ({
|
||||
})),
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
loadStaticCatalog: vi.fn<LoadStaticCatalog>(async () => []),
|
||||
resolveBundledStaticCatalogModel: vi.fn<ResolveStaticCatalog>(() => undefined),
|
||||
resolveStaticCatalogModel: vi.fn<StaticCatalogResolver>(() => undefined),
|
||||
createStaticCatalogResolver: vi.fn<CreateStaticCatalogResolver>(),
|
||||
configuredAgentIds: [] as string[],
|
||||
mutationListener: undefined as
|
||||
| ((event: { agentDir?: string; affectsInheritedStores: boolean }) => void)
|
||||
@@ -80,8 +82,8 @@ vi.mock("./runtime-plugins.js", () => ({
|
||||
vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({
|
||||
loadBundledProviderStaticCatalogContextModels: (...args: Parameters<LoadStaticCatalog>) =>
|
||||
mocks.loadStaticCatalog(...args),
|
||||
resolveBundledStaticCatalogModel: (...args: Parameters<ResolveStaticCatalog>) =>
|
||||
mocks.resolveBundledStaticCatalogModel(...args),
|
||||
createBundledStaticCatalogModelResolver: (...args: Parameters<CreateStaticCatalogResolver>) =>
|
||||
mocks.createStaticCatalogResolver(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
@@ -117,7 +119,9 @@ describe("prepared model runtime snapshots", () => {
|
||||
mocks.buildPreparedModelCatalogSnapshot.mockClear();
|
||||
mocks.ensureRuntimePluginsLoaded.mockClear();
|
||||
mocks.loadStaticCatalog.mockClear();
|
||||
mocks.resolveBundledStaticCatalogModel.mockClear();
|
||||
mocks.resolveStaticCatalogModel.mockReset();
|
||||
mocks.createStaticCatalogResolver.mockReset();
|
||||
mocks.createStaticCatalogResolver.mockReturnValue(mocks.resolveStaticCatalogModel);
|
||||
mocks.modelRegistry.fork.mockClear();
|
||||
mocks.configuredAgentIds = [];
|
||||
});
|
||||
@@ -286,7 +290,7 @@ describe("prepared model runtime snapshots", () => {
|
||||
contextWindow: 1_050_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
mocks.resolveBundledStaticCatalogModel.mockReturnValueOnce(runtimeModel);
|
||||
mocks.resolveStaticCatalogModel.mockReturnValueOnce(runtimeModel);
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -311,7 +315,17 @@ describe("prepared model runtime snapshots", () => {
|
||||
env: process.env,
|
||||
workspaceDir: "/tmp/prepared-model-runtime-manifest-workspace",
|
||||
});
|
||||
expect(mocks.resolveBundledStaticCatalogModel).toHaveBeenCalledOnce();
|
||||
expect(mocks.createStaticCatalogResolver).toHaveBeenCalledOnce();
|
||||
expect(mocks.createStaticCatalogResolver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: config,
|
||||
env: process.env,
|
||||
includeRuntimeDiscovery: true,
|
||||
metadataSnapshot: snapshot.metadataSnapshot,
|
||||
workspaceDir: "/tmp/prepared-model-runtime-manifest-workspace",
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveStaticCatalogModel).toHaveBeenCalledOnce();
|
||||
expect(snapshot.agentId).toBe("qa");
|
||||
expect(snapshot.configuredRuntimeModels).toEqual([
|
||||
{ provider: "openai", modelId: "gpt-5.4", model: runtimeModel },
|
||||
@@ -350,7 +364,7 @@ describe("prepared model runtime snapshots", () => {
|
||||
baseUrl: "https://provider-static.example.test/v1",
|
||||
},
|
||||
]);
|
||||
mocks.resolveBundledStaticCatalogModel.mockReturnValueOnce(runtimeModel);
|
||||
mocks.resolveStaticCatalogModel.mockReturnValueOnce(runtimeModel);
|
||||
|
||||
const snapshot = await publishPreparedModelRuntimeSnapshot({
|
||||
config: { agents: { defaults: { model: { primary: "nvidia/nemotron-static" } } } },
|
||||
|
||||
Reference in New Issue
Block a user