Skip to content

Commit 7d9dc8c

Browse files
committed
fix: reuse plugin manifests for model pricing refresh
1 parent 3af3431 commit 7d9dc8c

6 files changed

Lines changed: 207 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- Plugins/startup: use a `PluginLookUpTable` during Gateway startup so channel ownership, deferred channel loading, and startup plugin IDs reuse the same installed manifest registry instead of rebuilding manifest metadata on the boot path. Thanks @shakkernerd.
4646
- Plugins/startup: pass the Gateway `PluginLookUpTable` through plugin loading so auto-enable checks and startup-scope fallback reuse the same manifest registry instead of doing another manifest pass. Thanks @shakkernerd.
4747
- Plugins/startup: carry the Gateway `PluginLookUpTable` into deferred channel full-runtime reloads so post-listen startup does not rebuild manifest metadata after the provisional setup-runtime load. Thanks @shakkernerd.
48+
- Gateway/models: reuse Gateway plugin manifest metadata during the initial model-pricing refresh so pricing policies and configured plugin web-search models do not rebuild plugin lookups during startup. Thanks @shakkernerd.
4849
- Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd.
4950
- Plugins/channels: resolve read-only channel command defaults from one plugin index plus manifest pass instead of reloading plugin metadata while checking candidate plugin enablement. Thanks @shakkernerd.
5051
- Plugins/capabilities: cache manifest-derived capability provider plugin IDs per config snapshot so repeated TTS, media, realtime, memory, image, video, and music provider resolution avoids redundant manifest scans. Thanks @shakkernerd.

src/gateway/model-pricing-cache.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,38 @@ import { modelKey } from "../agents/model-selection.js";
33
import type { OpenClawConfig } from "../config/config.js";
44
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
55
import { loggingState } from "../logging/state.js";
6+
import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js";
67
import type { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
78
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
89

910
const normalizeProviderModelIdWithPluginMock = vi.hoisted(() =>
1011
vi.fn<typeof normalizeProviderModelIdWithPlugin>(({ context }) => context.modelId),
1112
);
13+
const pluginManifestRegistryMocks = vi.hoisted(() => ({
14+
manifestRegistry: undefined as PluginManifestRegistry | undefined,
15+
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
16+
}));
1217

1318
vi.mock("../plugins/provider-runtime.js", () => {
1419
return { normalizeProviderModelIdWithPlugin: normalizeProviderModelIdWithPluginMock };
1520
});
1621

22+
vi.mock("../plugins/manifest-registry-installed.js", async (importOriginal) => {
23+
const actual = await importOriginal<typeof import("../plugins/manifest-registry-installed.js")>();
24+
return {
25+
...actual,
26+
loadPluginManifestRegistryForInstalledIndex: (
27+
params: Parameters<typeof actual.loadPluginManifestRegistryForInstalledIndex>[0],
28+
) => {
29+
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex(params);
30+
return (
31+
pluginManifestRegistryMocks.manifestRegistry ??
32+
actual.loadPluginManifestRegistryForInstalledIndex(params)
33+
);
34+
},
35+
};
36+
});
37+
1738
import {
1839
__resetGatewayModelPricingCacheForTest,
1940
collectConfiguredModelPricingRefs,
@@ -25,6 +46,8 @@ import {
2546
describe("model-pricing-cache", () => {
2647
beforeEach(() => {
2748
__resetGatewayModelPricingCacheForTest();
49+
pluginManifestRegistryMocks.manifestRegistry = undefined;
50+
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockClear();
2851
});
2952

3053
afterEach(() => {
@@ -121,6 +144,48 @@ describe("model-pricing-cache", () => {
121144
expect(refs).toContain("tavily/search-preview");
122145
});
123146

147+
it("uses one installed manifest pass for pricing policies and configured web-search refs", async () => {
148+
pluginManifestRegistryMocks.manifestRegistry = {
149+
diagnostics: [],
150+
plugins: [
151+
createManifestRecord({
152+
id: "search-plugin",
153+
contracts: { webSearchProviders: ["search-plugin"] },
154+
}),
155+
],
156+
};
157+
const config = {
158+
plugins: {
159+
entries: {
160+
"search-plugin": {
161+
config: {
162+
webSearch: {
163+
model: "local-search/search-model",
164+
},
165+
},
166+
},
167+
},
168+
},
169+
models: {
170+
providers: {
171+
"local-search": {
172+
baseUrl: "http://127.0.0.1:43210/v1",
173+
api: "openai-completions",
174+
models: [{ id: "search-model" }],
175+
},
176+
},
177+
},
178+
} as unknown as OpenClawConfig;
179+
const fetchImpl = vi.fn<typeof fetch>();
180+
181+
await refreshGatewayModelPricingCache({ config, fetchImpl });
182+
183+
expect(
184+
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex,
185+
).toHaveBeenCalledOnce();
186+
expect(fetchImpl).not.toHaveBeenCalled();
187+
});
188+
124189
it("skips remote pricing catalogs for local-only model providers", async () => {
125190
const config = {
126191
agents: {
@@ -717,3 +782,19 @@ describe("model-pricing-cache", () => {
717782
});
718783
});
719784
});
785+
786+
function createManifestRecord(overrides: Partial<PluginManifestRecord>): PluginManifestRecord {
787+
return {
788+
id: "plugin",
789+
channels: [],
790+
providers: [],
791+
cliBackends: [],
792+
skills: [],
793+
hooks: [],
794+
origin: "global",
795+
rootDir: "/tmp/plugin",
796+
source: "/tmp/plugin/index.js",
797+
manifestPath: "/tmp/plugin/openclaw.plugin.json",
798+
...overrides,
799+
};
800+
}

src/gateway/model-pricing-cache.ts

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ import type { ModelDefinitionConfig } from "../config/types.models.js";
1212
import type { OpenClawConfig } from "../config/types.openclaw.js";
1313
import { createSubsystemLogger } from "../logging/subsystem.js";
1414
import { planManifestModelCatalogRows, type ModelCatalogCost } from "../model-catalog/index.js";
15+
import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js";
16+
import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js";
17+
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
1518
import type {
1619
PluginManifestModelPricingModelIdTransform,
1720
PluginManifestModelPricingProvider,
1821
PluginManifestModelPricingSource,
1922
} from "../plugins/manifest.js";
23+
import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
2024
import {
21-
loadPluginManifestRegistryForPluginRegistry,
22-
resolveManifestContractPluginIds,
25+
loadPluginRegistrySnapshot,
26+
type PluginRegistrySnapshot,
2327
} from "../plugins/plugin-registry.js";
2428
import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
2529
import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js";
@@ -39,6 +43,11 @@ type OpenRouterPricingEntry = {
3943

4044
type ModelListLike = string | { primary?: string; fallbacks?: string[] } | undefined;
4145

46+
type ModelPricingManifestMetadata = {
47+
allRegistry: PluginManifestRegistry;
48+
activeRegistry: PluginManifestRegistry;
49+
};
50+
4251
type OpenRouterModelPayload = {
4352
id?: unknown;
4453
pricing?: unknown;
@@ -361,11 +370,60 @@ function normalizeExternalPricingPolicy(
361370
};
362371
}
363372

364-
function loadManifestPricingContext(config: OpenClawConfig): {
373+
function filterActiveManifestRegistry(params: {
374+
registry: PluginManifestRegistry;
375+
index: PluginRegistrySnapshot;
376+
config: OpenClawConfig;
377+
}): PluginManifestRegistry {
378+
return {
379+
diagnostics: params.registry.diagnostics,
380+
plugins: params.registry.plugins.filter((plugin) =>
381+
isInstalledPluginEnabled(params.index, plugin.id, params.config),
382+
),
383+
};
384+
}
385+
386+
function resolveModelPricingManifestMetadata(params: {
387+
config: OpenClawConfig;
388+
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
389+
manifestRegistry?: PluginManifestRegistry;
390+
}): ModelPricingManifestMetadata {
391+
if (params.pluginLookUpTable) {
392+
return {
393+
allRegistry: params.pluginLookUpTable.manifestRegistry,
394+
activeRegistry: filterActiveManifestRegistry({
395+
registry: params.pluginLookUpTable.manifestRegistry,
396+
index: params.pluginLookUpTable.index,
397+
config: params.config,
398+
}),
399+
};
400+
}
401+
if (params.manifestRegistry) {
402+
return {
403+
allRegistry: params.manifestRegistry,
404+
activeRegistry: params.manifestRegistry,
405+
};
406+
}
407+
const index = loadPluginRegistrySnapshot({ config: params.config });
408+
const allRegistry = loadPluginManifestRegistryForInstalledIndex({
409+
index,
410+
config: params.config,
411+
includeDisabled: true,
412+
});
413+
return {
414+
allRegistry,
415+
activeRegistry: filterActiveManifestRegistry({
416+
registry: allRegistry,
417+
index,
418+
config: params.config,
419+
}),
420+
};
421+
}
422+
423+
function loadManifestPricingContext(registry: PluginManifestRegistry): {
365424
policies: Map<string, ExternalPricingPolicy>;
366425
catalogPricing: Map<string, CachedModelPricing>;
367426
} {
368-
const registry = loadPluginManifestRegistryForPluginRegistry({ config });
369427
const policies = new Map<string, ExternalPricingPolicy>();
370428
for (const plugin of registry.plugins) {
371429
for (const [provider, rawPolicy] of Object.entries(plugin.modelPricing?.providers ?? {})) {
@@ -549,11 +607,12 @@ function addConfiguredWebSearchPluginModels(params: {
549607
config: OpenClawConfig;
550608
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
551609
refs: Map<string, ModelRef>;
610+
manifestRegistry: PluginManifestRegistry;
552611
}): void {
553-
for (const pluginId of resolveManifestContractPluginIds({
554-
contract: "webSearchProviders",
555-
config: params.config,
556-
})) {
612+
for (const pluginId of params.manifestRegistry.plugins
613+
.filter((plugin) => (plugin.contracts?.webSearchProviders ?? []).length > 0)
614+
.map((plugin) => plugin.id)
615+
.toSorted((left, right) => left.localeCompare(right))) {
557616
addResolvedModelRef({
558617
raw: resolvePluginWebSearchConfig(params.config, pluginId)?.model as string | undefined,
559618
aliasIndex: params.aliasIndex,
@@ -659,7 +718,12 @@ function filterExternalPricingRefs(params: {
659718
);
660719
}
661720

662-
export function collectConfiguredModelPricingRefs(config: OpenClawConfig): ModelRef[] {
721+
export function collectConfiguredModelPricingRefs(
722+
config: OpenClawConfig,
723+
options: { manifestRegistry?: PluginManifestRegistry } = {},
724+
): ModelRef[] {
725+
const manifestRegistry =
726+
options.manifestRegistry ?? resolveModelPricingManifestMetadata({ config }).allRegistry;
663727
const refs = new Map<string, ModelRef>();
664728
const aliasIndex = buildModelAliasIndex({
665729
cfg: config,
@@ -698,7 +762,7 @@ export function collectConfiguredModelPricingRefs(config: OpenClawConfig): Model
698762
}
699763
}
700764

701-
addConfiguredWebSearchPluginModels({ config, aliasIndex, refs });
765+
addConfiguredWebSearchPluginModels({ config, aliasIndex, refs, manifestRegistry });
702766

703767
for (const entry of config.tools?.media?.models ?? []) {
704768
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
@@ -823,14 +887,23 @@ function collectSeededPricing(params: {
823887
export async function refreshGatewayModelPricingCache(params: {
824888
config: OpenClawConfig;
825889
fetchImpl?: typeof fetch;
890+
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
891+
manifestRegistry?: PluginManifestRegistry;
826892
}): Promise<void> {
827893
if (inFlightRefresh) {
828894
return await inFlightRefresh;
829895
}
830896
const fetchImpl = params.fetchImpl ?? fetch;
831897
inFlightRefresh = (async () => {
832-
const pricingContext = loadManifestPricingContext(params.config);
833-
const allRefs = collectConfiguredModelPricingRefs(params.config);
898+
const manifestMetadata = resolveModelPricingManifestMetadata({
899+
config: params.config,
900+
pluginLookUpTable: params.pluginLookUpTable,
901+
manifestRegistry: params.manifestRegistry,
902+
});
903+
const pricingContext = loadManifestPricingContext(manifestMetadata.activeRegistry);
904+
const allRefs = collectConfiguredModelPricingRefs(params.config, {
905+
manifestRegistry: manifestMetadata.allRegistry,
906+
});
834907
const seededPricing = collectSeededPricing({
835908
config: params.config,
836909
refs: allRefs,
@@ -950,6 +1023,8 @@ export async function refreshGatewayModelPricingCache(params: {
9501023
export function startGatewayModelPricingRefresh(params: {
9511024
config: OpenClawConfig;
9521025
fetchImpl?: typeof fetch;
1026+
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
1027+
manifestRegistry?: PluginManifestRegistry;
9531028
}): () => void {
9541029
let stopped = false;
9551030
queueMicrotask(() => {

src/gateway/server-runtime-services.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const hoisted = vi.hoisted(() => {
1010
startHeartbeatRunner: vi.fn(() => heartbeatRunner),
1111
startChannelHealthMonitor: vi.fn(() => ({ stop: vi.fn() })),
1212
startGatewayModelPricingRefresh: vi.fn(() => vi.fn()),
13+
isVitestRuntimeEnv: vi.fn(() => false),
1314
recoverPendingDeliveries: vi.fn(async () => undefined),
1415
recoverPendingRestartContinuationDeliveries: vi.fn(async () => undefined),
1516
deliverOutboundPayloads: vi.fn(),
@@ -20,6 +21,10 @@ vi.mock("../infra/heartbeat-runner.js", () => ({
2021
startHeartbeatRunner: hoisted.startHeartbeatRunner,
2122
}));
2223

24+
vi.mock("../infra/env.js", () => ({
25+
isVitestRuntimeEnv: hoisted.isVitestRuntimeEnv,
26+
}));
27+
2328
vi.mock("../infra/outbound/deliver.js", () => ({
2429
deliverOutboundPayloads: hoisted.deliverOutboundPayloads,
2530
}));
@@ -51,6 +56,7 @@ describe("server-runtime-services", () => {
5156
hoisted.startHeartbeatRunner.mockClear();
5257
hoisted.startChannelHealthMonitor.mockClear();
5358
hoisted.startGatewayModelPricingRefresh.mockClear();
59+
hoisted.isVitestRuntimeEnv.mockReset().mockReturnValue(false);
5460
hoisted.recoverPendingDeliveries.mockClear();
5561
hoisted.recoverPendingRestartContinuationDeliveries.mockClear();
5662
hoisted.deliverOutboundPayloads.mockClear();
@@ -69,13 +75,38 @@ describe("server-runtime-services", () => {
6975
});
7076

7177
expect(hoisted.startChannelHealthMonitor).toHaveBeenCalledTimes(1);
78+
expect(hoisted.startGatewayModelPricingRefresh).toHaveBeenCalledWith({ config: {} });
7279
expect(hoisted.startHeartbeatRunner).not.toHaveBeenCalled();
7380
expect(hoisted.recoverPendingDeliveries).not.toHaveBeenCalled();
7481

7582
services.heartbeatRunner.stop();
7683
expect(hoisted.heartbeatRunner.stop).not.toHaveBeenCalled();
7784
});
7885

86+
it("passes startup plugin lookup metadata to the initial pricing refresh", () => {
87+
const pluginLookUpTable = {
88+
index: { plugins: [] },
89+
manifestRegistry: { plugins: [], diagnostics: [] },
90+
};
91+
92+
startGatewayRuntimeServices({
93+
minimalTestGateway: false,
94+
cfgAtStart: {} as never,
95+
channelManager: {
96+
getRuntimeSnapshot: vi.fn(),
97+
isHealthMonitorEnabled: vi.fn(),
98+
isManuallyStopped: vi.fn(),
99+
} as never,
100+
log: createLog(),
101+
pluginLookUpTable: pluginLookUpTable as never,
102+
});
103+
104+
expect(hoisted.startGatewayModelPricingRefresh).toHaveBeenCalledWith({
105+
config: {},
106+
pluginLookUpTable,
107+
});
108+
});
109+
79110
it("activates heartbeat, cron, and delivery recovery after sidecars are ready", async () => {
80111
vi.useFakeTimers();
81112
const cron = { start: vi.fn(async () => undefined) };

src/gateway/server-runtime-services.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import { isVitestRuntimeEnv } from "../infra/env.js";
33
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
4+
import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
45
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
56
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
67
import { startGatewayModelPricingRefresh } from "./model-pricing-cache.js";
@@ -93,6 +94,7 @@ export function startGatewayRuntimeServices(params: {
9394
cfgAtStart: OpenClawConfig;
9495
channelManager: GatewayChannelManager;
9596
log: GatewayRuntimeServiceLogger;
97+
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
9698
}): {
9799
heartbeatRunner: HeartbeatRunner;
98100
channelHealthMonitor: ChannelHealthMonitor | null;
@@ -108,7 +110,10 @@ export function startGatewayRuntimeServices(params: {
108110
channelHealthMonitor,
109111
stopModelPricingRefresh:
110112
!params.minimalTestGateway && !isVitestRuntimeEnv()
111-
? startGatewayModelPricingRefresh({ config: params.cfgAtStart })
113+
? startGatewayModelPricingRefresh({
114+
config: params.cfgAtStart,
115+
...(params.pluginLookUpTable ? { pluginLookUpTable: params.pluginLookUpTable } : {}),
116+
})
112117
: () => {},
113118
};
114119
}

src/gateway/server.impl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@ export async function startGatewayServer(
761761
cfgAtStart,
762762
channelManager,
763763
log,
764+
pluginLookUpTable,
764765
}),
765766
);
766767

0 commit comments

Comments
 (0)