mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(plugins): reuse workspace metadata for session model refs (#76655)
Fixes the session-list plugin metadata hot path by reusing the active workspace-scoped plugin metadata snapshot for manifest model-id normalization and setup CLI backend fallback. Also keeps model-pricing collection on its provided manifest registry and fixes the CI-only hoisted test mock failure. Validated with targeted plugin/gateway/model-selection tests, CI-shaped gateway startup shard, Testbox `pnpm check:changed`, and green PR CI. Thanks @rolandrscheel.
This commit is contained in:
@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/install: require OpenClaw-owned install provenance before granting official npm plugin scanner trust, so direct npm package names no longer bypass launch-code scanning while catalog, onboarding, and doctor installs stay trusted. Thanks @fede-kamel and @vincentkoc.
|
||||
- Network proxy: preserve target TLS hostname validation for Node HTTPS requests routed through the managed HTTP proxy, so Discord-style CONNECT traffic no longer validates certificates against the local proxy host. Fixes #74809. (#76442) Thanks @jesse-merhi and @abnershang.
|
||||
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
|
||||
- Gateway/sessions: cache manifest model-id normalization and bundled setup CLI fallback metadata against the active plugin metadata snapshot, so Control UI `sessions.list` polling avoids repeated plugin manifest scans while still refreshing after plugin reloads. Thanks @rolandrscheel.
|
||||
- Gateway/performance: cache per-run verbose-level session reads, skip a redundant `lsof` scan in `gateway --force` when no listener was killed, and make the Gateway startup benchmark print usage for `--help`.
|
||||
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows so model-only session patches do not make Control UI lose runtime identity. Thanks @vincentkoc.
|
||||
- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { normalizeProviderModelIdWithManifest } from "../plugins/manifest-model-id-normalization.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
@@ -26,7 +27,10 @@ export function modelKey(provider: string, model: string): string {
|
||||
export function normalizeStaticProviderModelId(
|
||||
provider: string,
|
||||
model: string,
|
||||
options: { allowManifestNormalization?: boolean } = {},
|
||||
options: {
|
||||
allowManifestNormalization?: boolean;
|
||||
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
} = {},
|
||||
): string {
|
||||
if (options.allowManifestNormalization === false) {
|
||||
return model;
|
||||
@@ -34,6 +38,7 @@ export function normalizeStaticProviderModelId(
|
||||
return (
|
||||
normalizeProviderModelIdWithManifest({
|
||||
provider,
|
||||
plugins: options.manifestPlugins,
|
||||
context: {
|
||||
provider,
|
||||
modelId: model,
|
||||
|
||||
@@ -13,7 +13,7 @@ export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean {
|
||||
if (cliBackends.some((backend) => normalizeProviderId(backend.id) === normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (resolvePluginSetupCliBackendRuntime({ backend: normalized })) {
|
||||
if (resolvePluginSetupCliBackendRuntime({ backend: normalized, config: cfg })) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { modelKey as sharedModelKey, normalizeStaticProviderModelId } from "./model-ref-shared.js";
|
||||
import {
|
||||
@@ -38,10 +39,15 @@ export {
|
||||
function normalizeProviderModelId(
|
||||
provider: string,
|
||||
model: string,
|
||||
options?: { allowManifestNormalization?: boolean; allowPluginNormalization?: boolean },
|
||||
options?: {
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
},
|
||||
): string {
|
||||
const staticModelId = normalizeStaticProviderModelId(provider, model, {
|
||||
allowManifestNormalization: options?.allowManifestNormalization,
|
||||
manifestPlugins: options?.manifestPlugins,
|
||||
});
|
||||
if (options?.allowPluginNormalization === false) {
|
||||
return staticModelId;
|
||||
@@ -60,6 +66,7 @@ function normalizeProviderModelId(
|
||||
type ModelRefNormalizeOptions = {
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
};
|
||||
|
||||
export function normalizeModelRef(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -35,6 +36,10 @@ export type ModelAliasIndex = {
|
||||
byKey: Map<string, string[]>;
|
||||
};
|
||||
|
||||
type ManifestNormalizationContext = {
|
||||
manifestPlugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
};
|
||||
|
||||
function sanitizeModelWarningValue(value: string): string {
|
||||
const stripped = value ? stripAnsi(value) : "";
|
||||
let controlBoundary = -1;
|
||||
@@ -179,12 +184,14 @@ function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean {
|
||||
return ref.provider === "openrouter" && ref.model.includes("/") && ref.model.endsWith(":free");
|
||||
}
|
||||
|
||||
function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
function resolveConfiguredOpenRouterCompatFreeRef(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): ModelRef | null {
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const raw of Object.keys(configuredModels)) {
|
||||
if (!raw.includes("/")) {
|
||||
@@ -193,6 +200,7 @@ function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
const parsed = parseModelRef(raw, params.defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (parsed && isConcreteOpenRouterFreeModelRef(parsed)) {
|
||||
return parsed;
|
||||
@@ -211,24 +219,28 @@ function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
return normalizeModelRef("openrouter", modelId, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
export function resolveConfiguredOpenRouterCompatAlias(
|
||||
params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): ModelRef | null {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(params.raw);
|
||||
if (normalized === "openrouter:auto") {
|
||||
return normalizeModelRef("openrouter", "auto", {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
}
|
||||
if (normalized !== OPENROUTER_COMPAT_FREE_ALIAS || !params.cfg) {
|
||||
@@ -239,32 +251,38 @@ export function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
}
|
||||
|
||||
function parseModelRefWithCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
function parseModelRefWithCompatAlias(
|
||||
params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): ModelRef | null {
|
||||
return (
|
||||
resolveConfiguredOpenRouterCompatAlias(params) ??
|
||||
resolveExactConfiguredProviderRef(params) ??
|
||||
parseModelRef(params.raw, params.defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function resolveExactConfiguredProviderRef(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
function resolveExactConfiguredProviderRef(
|
||||
params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): ModelRef | null {
|
||||
const slash = params.raw.indexOf("/");
|
||||
if (slash <= 0 || !params.cfg?.models?.providers) {
|
||||
return null;
|
||||
@@ -293,6 +311,7 @@ function resolveExactConfiguredProviderRef(params: {
|
||||
provider,
|
||||
model: normalizeStaticProviderModelId(provider, modelRaw.trim(), {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -336,12 +355,14 @@ export function buildConfiguredAllowlistKeys(params: {
|
||||
return keys.size > 0 ? keys : null;
|
||||
}
|
||||
|
||||
export function buildModelAliasIndex(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelAliasIndex {
|
||||
export function buildModelAliasIndex(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): ModelAliasIndex {
|
||||
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
|
||||
const byKey = new Map<string, string[]>();
|
||||
|
||||
@@ -353,6 +374,7 @@ export function buildModelAliasIndex(params: {
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
@@ -459,14 +481,16 @@ function buildSyntheticAllowedCatalogEntry(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveModelRefFromString(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): { ref: ModelRef; alias?: string } | null {
|
||||
export function resolveModelRefFromString(
|
||||
params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
} & ManifestNormalizationContext,
|
||||
): { ref: ModelRef; alias?: string } | null {
|
||||
const { model } = splitTrailingAuthProfile(params.raw);
|
||||
if (!model) {
|
||||
return null;
|
||||
@@ -482,6 +506,7 @@ export function resolveModelRefFromString(params: {
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (!parsed) {
|
||||
return null;
|
||||
|
||||
@@ -78,6 +78,7 @@ export { getCachedGatewayModelPricing };
|
||||
type PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
};
|
||||
|
||||
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
||||
@@ -99,13 +100,15 @@ function clearRefreshTimer(): void {
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
function getPricingModelNormalizationOptions(
|
||||
config: OpenClawConfig,
|
||||
): PricingModelNormalizationOptions {
|
||||
const allowPluginBackedNormalization = config.plugins?.enabled !== false;
|
||||
function getPricingModelNormalizationOptions(params: {
|
||||
config: OpenClawConfig;
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
}): PricingModelNormalizationOptions {
|
||||
const allowPluginBackedNormalization = params.config.plugins?.enabled !== false;
|
||||
return {
|
||||
allowManifestNormalization: allowPluginBackedNormalization,
|
||||
allowPluginNormalization: allowPluginBackedNormalization,
|
||||
...(params.manifestRegistry ? { manifestPlugins: params.manifestRegistry.plugins } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,13 +374,14 @@ async function fetchLiteLLMPricingCatalog(
|
||||
|
||||
function normalizeExternalPricingSource(
|
||||
value: PluginManifestModelPricingSource | false | undefined,
|
||||
options: PricingModelNormalizationOptions,
|
||||
): ExternalPricingSourcePolicy | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(value.provider
|
||||
? { provider: normalizeModelRef(value.provider, "placeholder").provider }
|
||||
? { provider: normalizeModelRef(value.provider, "placeholder", options).provider }
|
||||
: {}),
|
||||
...(value.passthroughProviderModel ? { passthroughProviderModel: true } : {}),
|
||||
modelIdTransforms: value.modelIdTransforms ?? [],
|
||||
@@ -386,17 +390,18 @@ function normalizeExternalPricingSource(
|
||||
|
||||
function normalizeExternalPricingPolicy(
|
||||
value: PluginManifestModelPricingProvider | undefined,
|
||||
options: PricingModelNormalizationOptions,
|
||||
): ExternalPricingPolicy | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
external: value.external !== false,
|
||||
...(normalizeExternalPricingSource(value.openRouter) !== undefined
|
||||
? { openRouter: normalizeExternalPricingSource(value.openRouter) }
|
||||
...(normalizeExternalPricingSource(value.openRouter, options) !== undefined
|
||||
? { openRouter: normalizeExternalPricingSource(value.openRouter, options) }
|
||||
: {}),
|
||||
...(normalizeExternalPricingSource(value.liteLLM) !== undefined
|
||||
? { liteLLM: normalizeExternalPricingSource(value.liteLLM) }
|
||||
...(normalizeExternalPricingSource(value.liteLLM, options) !== undefined
|
||||
? { liteLLM: normalizeExternalPricingSource(value.liteLLM, options) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -461,14 +466,17 @@ function resolveModelPricingManifestMetadata(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function loadManifestPricingContext(registry: PluginManifestRegistry): {
|
||||
function loadManifestPricingContext(
|
||||
registry: PluginManifestRegistry,
|
||||
normalizationOptions: PricingModelNormalizationOptions,
|
||||
): {
|
||||
policies: Map<string, ExternalPricingPolicy>;
|
||||
catalogPricing: Map<string, CachedModelPricing>;
|
||||
} {
|
||||
const policies = new Map<string, ExternalPricingPolicy>();
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const [provider, rawPolicy] of Object.entries(plugin.modelPricing?.providers ?? {})) {
|
||||
const policy = normalizeExternalPricingPolicy(rawPolicy);
|
||||
const policy = normalizeExternalPricingPolicy(rawPolicy, normalizationOptions);
|
||||
if (policy) {
|
||||
policies.set(provider, policy);
|
||||
}
|
||||
@@ -531,6 +539,7 @@ function canonicalizeOpenRouterLookupId(
|
||||
const provider = normalizeModelRef(trimmed.slice(0, slash), "placeholder", {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
manifestPlugins: options.manifestPlugins,
|
||||
}).provider;
|
||||
const model = trimmed.slice(slash + 1).trim();
|
||||
if (!model) {
|
||||
@@ -539,6 +548,7 @@ function canonicalizeOpenRouterLookupId(
|
||||
const normalizedModel = normalizeModelRef(provider, model, {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
manifestPlugins: options.manifestPlugins,
|
||||
}).model;
|
||||
return modelKey(provider, normalizedModel);
|
||||
}
|
||||
@@ -550,6 +560,7 @@ function buildExternalCatalogCandidates(params: {
|
||||
seen?: Set<string>;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): string[] {
|
||||
const { ref, source, policies } = params;
|
||||
const refKey = modelKey(ref.provider, ref.model);
|
||||
@@ -582,6 +593,7 @@ function buildExternalCatalogCandidates(params: {
|
||||
? canonicalizeOpenRouterLookupId(candidate, {
|
||||
allowManifestNormalization: params.allowManifestNormalization ?? true,
|
||||
allowPluginNormalization: params.allowPluginNormalization ?? true,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
})
|
||||
: candidate,
|
||||
);
|
||||
@@ -591,6 +603,7 @@ function buildExternalCatalogCandidates(params: {
|
||||
const nestedRef = parseModelRef(ref.model, DEFAULT_PROVIDER, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (nestedRef) {
|
||||
for (const candidate of buildExternalCatalogCandidates({
|
||||
@@ -600,6 +613,7 @@ function buildExternalCatalogCandidates(params: {
|
||||
seen: nextSeen,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
})) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
@@ -615,6 +629,7 @@ function addResolvedModelRef(params: {
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): void {
|
||||
const raw = params.raw?.trim();
|
||||
if (!raw) {
|
||||
@@ -626,6 +641,7 @@ function addResolvedModelRef(params: {
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (!resolved) {
|
||||
return;
|
||||
@@ -633,6 +649,7 @@ function addResolvedModelRef(params: {
|
||||
const normalized = normalizeModelRef(resolved.ref.provider, resolved.ref.model, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
params.refs.set(modelKey(normalized.provider, normalized.model), normalized);
|
||||
}
|
||||
@@ -643,6 +660,7 @@ function addModelListLike(params: {
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): void {
|
||||
addResolvedModelRef({
|
||||
raw: resolvePrimaryStringValue(params.value),
|
||||
@@ -650,6 +668,7 @@ function addModelListLike(params: {
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
for (const fallback of listLikeFallbacks(params.value)) {
|
||||
addResolvedModelRef({
|
||||
@@ -658,6 +677,7 @@ function addModelListLike(params: {
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -668,6 +688,7 @@ function addProviderModelPair(params: {
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): void {
|
||||
const provider = params.provider?.trim();
|
||||
const model = params.model?.trim();
|
||||
@@ -677,6 +698,7 @@ function addProviderModelPair(params: {
|
||||
const normalized = normalizeModelRef(provider, model, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
params.refs.set(modelKey(normalized.provider, normalized.model), normalized);
|
||||
}
|
||||
@@ -688,6 +710,7 @@ function addConfiguredWebSearchPluginModels(params: {
|
||||
manifestRegistry: PluginManifestRegistry;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): void {
|
||||
for (const pluginId of params.manifestRegistry.plugins
|
||||
.filter((plugin) => (plugin.contracts?.webSearchProviders ?? []).length > 0)
|
||||
@@ -699,6 +722,7 @@ function addConfiguredWebSearchPluginModels(params: {
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -752,6 +776,7 @@ function findConfiguredProviderModel(
|
||||
const normalized = normalizeModelRef(ref.provider, model.id, {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
manifestPlugins: options.manifestPlugins,
|
||||
});
|
||||
return modelKey(normalized.provider, normalized.model) === modelKey(ref.provider, ref.model);
|
||||
});
|
||||
@@ -791,6 +816,7 @@ function shouldFetchExternalPricingForRef(params: {
|
||||
seededPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): boolean {
|
||||
if (params.seededPricing.has(modelKey(params.ref.provider, params.ref.model))) {
|
||||
return false;
|
||||
@@ -799,6 +825,7 @@ function shouldFetchExternalPricingForRef(params: {
|
||||
hasPrivateOrLoopbackConfiguredEndpoint(params.config, params.ref, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
@@ -816,6 +843,7 @@ function filterExternalPricingRefs(params: {
|
||||
seededPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): ModelRef[] {
|
||||
return params.refs.filter((ref) =>
|
||||
shouldFetchExternalPricingForRef({
|
||||
@@ -825,6 +853,7 @@ function filterExternalPricingRefs(params: {
|
||||
seededPricing: params.seededPricing,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -835,70 +864,71 @@ export function collectConfiguredModelPricingRefs(
|
||||
): ModelRef[] {
|
||||
const manifestRegistry =
|
||||
options.manifestRegistry ?? resolveModelPricingManifestMetadata({ config }).allRegistry;
|
||||
const normalizationOptions = getPricingModelNormalizationOptions(config);
|
||||
const normalizationOptions = getPricingModelNormalizationOptions({
|
||||
config,
|
||||
manifestRegistry,
|
||||
});
|
||||
const refs = new Map<string, ModelRef>();
|
||||
const normalizationParams = {
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...(normalizationOptions.manifestPlugins
|
||||
? { manifestPlugins: normalizationOptions.manifestPlugins }
|
||||
: {}),
|
||||
};
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: config,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.imageModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.pdfModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.agents?.defaults?.compaction?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.agents?.defaults?.heartbeat?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.tools?.subagents?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.messages?.tts?.summaryModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.hooks?.gmail?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
|
||||
for (const agent of config.agents?.list ?? []) {
|
||||
@@ -906,22 +936,19 @@ export function collectConfiguredModelPricingRefs(
|
||||
value: agent.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addModelListLike({
|
||||
value: agent.subagents?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: agent.heartbeat?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -930,8 +957,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
raw: mapping.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -944,8 +970,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
raw: typeof raw === "string" ? raw : undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -955,8 +980,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
aliasIndex,
|
||||
refs,
|
||||
manifestRegistry,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
|
||||
for (const entry of config.tools?.media?.models ?? []) {
|
||||
@@ -964,8 +988,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.image?.models ?? []) {
|
||||
@@ -973,8 +996,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.audio?.models ?? []) {
|
||||
@@ -982,8 +1004,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.video?.models ?? []) {
|
||||
@@ -991,8 +1012,7 @@ export function collectConfiguredModelPricingRefs(
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1032,6 +1052,7 @@ function resolveCatalogPricingForRef(params: {
|
||||
catalogByNormalizedId: Map<string, OpenRouterPricingEntry>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): CachedModelPricing | undefined {
|
||||
const candidates = buildExternalCatalogCandidates({
|
||||
ref: params.ref,
|
||||
@@ -1039,6 +1060,7 @@ function resolveCatalogPricingForRef(params: {
|
||||
policies: params.policies,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
for (const candidate of candidates) {
|
||||
const exact = params.catalogById.get(candidate);
|
||||
@@ -1050,6 +1072,7 @@ function resolveCatalogPricingForRef(params: {
|
||||
const normalized = canonicalizeOpenRouterLookupId(candidate, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (!normalized) {
|
||||
continue;
|
||||
@@ -1068,6 +1091,7 @@ function resolveLiteLLMPricingForRef(params: {
|
||||
catalog: LiteLLMPricingCatalog;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): CachedModelPricing | undefined {
|
||||
for (const candidate of buildExternalCatalogCandidates({
|
||||
ref: params.ref,
|
||||
@@ -1075,6 +1099,7 @@ function resolveLiteLLMPricingForRef(params: {
|
||||
policies: params.policies,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
})) {
|
||||
const pricing = params.catalog.get(candidate);
|
||||
if (pricing) {
|
||||
@@ -1109,6 +1134,7 @@ function collectSeededPricing(params: {
|
||||
catalogPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
manifestPlugins?: PluginManifestRegistry["plugins"];
|
||||
}): Map<string, CachedModelPricing> {
|
||||
const seeded = new Map<string, CachedModelPricing>();
|
||||
for (const ref of params.refs) {
|
||||
@@ -1116,6 +1142,7 @@ function collectSeededPricing(params: {
|
||||
const configuredPricing = getConfiguredModelPricing(params.config, ref, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
manifestPlugins: params.manifestPlugins,
|
||||
});
|
||||
if (configuredPricing) {
|
||||
seeded.set(key, configuredPricing);
|
||||
@@ -1152,8 +1179,21 @@ export async function refreshGatewayModelPricingCache(
|
||||
pluginLookUpTable: params.pluginLookUpTable,
|
||||
manifestRegistry: params.manifestRegistry,
|
||||
});
|
||||
const normalizationOptions = getPricingModelNormalizationOptions(params.config);
|
||||
const pricingContext = loadManifestPricingContext(manifestMetadata.activeRegistry);
|
||||
const normalizationOptions = getPricingModelNormalizationOptions({
|
||||
config: params.config,
|
||||
manifestRegistry: manifestMetadata.allRegistry,
|
||||
});
|
||||
const normalizationParams = {
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...(normalizationOptions.manifestPlugins
|
||||
? { manifestPlugins: normalizationOptions.manifestPlugins }
|
||||
: {}),
|
||||
};
|
||||
const pricingContext = loadManifestPricingContext(
|
||||
manifestMetadata.activeRegistry,
|
||||
normalizationOptions,
|
||||
);
|
||||
const allRefs = collectConfiguredModelPricingRefs(params.config, {
|
||||
manifestRegistry: manifestMetadata.allRegistry,
|
||||
});
|
||||
@@ -1161,16 +1201,14 @@ export async function refreshGatewayModelPricingCache(
|
||||
config: params.config,
|
||||
refs: allRefs,
|
||||
catalogPricing: pricingContext.catalogPricing,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
const refs = filterExternalPricingRefs({
|
||||
config: params.config,
|
||||
refs: allRefs,
|
||||
policies: pricingContext.policies,
|
||||
seededPricing,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
if (refs.length === 0) {
|
||||
if (params.signal?.aborted) {
|
||||
@@ -1219,8 +1257,7 @@ export async function refreshGatewayModelPricingCache(
|
||||
policies: pricingContext.policies,
|
||||
catalogById,
|
||||
catalogByNormalizedId,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
|
||||
// 2. Try LiteLLM (may contain tiered pricing)
|
||||
@@ -1228,8 +1265,7 @@ export async function refreshGatewayModelPricingCache(
|
||||
ref,
|
||||
policies: pricingContext.policies,
|
||||
catalog: litellmCatalog,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
...normalizationParams,
|
||||
});
|
||||
|
||||
// Merge strategy: OpenRouter provides the base flat pricing;
|
||||
|
||||
@@ -84,6 +84,7 @@ describe("gateway startup web fetch config", () => {
|
||||
await writeConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "loopback",
|
||||
auth: { mode: "none" },
|
||||
},
|
||||
plugins: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { vi } from "vitest";
|
||||
import { createGatewayConfigModuleMock } from "./test-helpers.config-runtime.js";
|
||||
import {
|
||||
getTestPluginRegistry,
|
||||
resetTestPluginRegistry,
|
||||
@@ -201,6 +200,7 @@ vi.mock("../config/sessions.js", async () => {
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
const { createGatewayConfigModuleMock } = await import("./test-helpers.config-runtime.js");
|
||||
return createGatewayConfigModuleMock(actual);
|
||||
});
|
||||
|
||||
@@ -208,6 +208,7 @@ vi.mock("../config/io.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/io.js")>("../config/io.js");
|
||||
const configActual =
|
||||
await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
const { createGatewayConfigModuleMock } = await import("./test-helpers.config-runtime.js");
|
||||
const configMock = createGatewayConfigModuleMock(configActual);
|
||||
const createConfigIO = vi.fn(() => ({
|
||||
...actual.createConfigIO(),
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearCurrentPluginMetadataSnapshot,
|
||||
resolvePluginMetadataControlPlaneFingerprint,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
const ORIGINAL_ENV = {
|
||||
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
|
||||
@@ -49,10 +59,17 @@ function writeInstallIndex(params: { stateDir: string; pluginDir: string }): voi
|
||||
|
||||
function writeNormalizerManifest(params: { pluginDir: string; prefix: string }): void {
|
||||
fs.mkdirSync(params.pluginDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(params.pluginDir, "index.ts"),
|
||||
"throw new Error('runtime entry should not load while reading manifests');\n",
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(params.pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "normalizer",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["demo"],
|
||||
modelIdNormalization: {
|
||||
providers: {
|
||||
demo: {
|
||||
@@ -65,6 +82,68 @@ function writeNormalizerManifest(params: { pluginDir: string; prefix: string }):
|
||||
);
|
||||
}
|
||||
|
||||
function createCurrentSnapshot(params: {
|
||||
manifestHash: string;
|
||||
prefix: string;
|
||||
workspaceDir?: string;
|
||||
}): PluginMetadataSnapshot {
|
||||
const policyHash = resolveInstalledPluginIndexPolicyHash({});
|
||||
const index: InstalledPluginIndex = {
|
||||
version: 1,
|
||||
hostContractVersion: "test-host",
|
||||
compatRegistryVersion: "test-compat",
|
||||
migrationVersion: 1,
|
||||
policyHash,
|
||||
generatedAtMs: 0,
|
||||
installRecords: {},
|
||||
plugins: [
|
||||
{
|
||||
pluginId: "normalizer",
|
||||
manifestPath: `/tmp/normalizer-${params.manifestHash}/openclaw.plugin.json`,
|
||||
manifestHash: params.manifestHash,
|
||||
source: `/tmp/normalizer-${params.manifestHash}/index.ts`,
|
||||
rootDir: `/tmp/normalizer-${params.manifestHash}`,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
return {
|
||||
policyHash,
|
||||
configFingerprint: resolvePluginMetadataControlPlaneFingerprint(
|
||||
{},
|
||||
{
|
||||
env: process.env,
|
||||
index,
|
||||
policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
},
|
||||
),
|
||||
workspaceDir: params.workspaceDir,
|
||||
index,
|
||||
plugins: [
|
||||
{
|
||||
id: "normalizer",
|
||||
modelIdNormalization: {
|
||||
providers: {
|
||||
demo: {
|
||||
prefixWhenBare: params.prefix,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as PluginMetadataSnapshot;
|
||||
}
|
||||
|
||||
function normalizeDemoModel(modelId = "demo-model"): string | undefined {
|
||||
return normalizeProviderModelIdWithManifest({
|
||||
provider: "demo",
|
||||
@@ -73,13 +152,73 @@ function normalizeDemoModel(modelId = "demo-model"): string | undefined {
|
||||
}
|
||||
|
||||
describe("manifest model id normalization", () => {
|
||||
beforeEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
resetPluginRuntimeStateForTest();
|
||||
restoreEnv();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes cached policies when the current metadata snapshot changes", () => {
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "alpha",
|
||||
prefix: "alpha",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
||||
expect(normalizeDemoModel("second-model")).toBe("alpha/second-model");
|
||||
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "bravo",
|
||||
prefix: "bravo",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(normalizeDemoModel()).toBe("bravo/demo-model");
|
||||
});
|
||||
|
||||
it("uses workspace-scoped current metadata through the active plugin runtime", () => {
|
||||
setActivePluginRegistry(
|
||||
createEmptyPluginRegistry(),
|
||||
"workspace-a",
|
||||
"gateway-bindable",
|
||||
"/workspace/a",
|
||||
);
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "alpha",
|
||||
prefix: "alpha",
|
||||
workspaceDir: "/workspace/a",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
||||
expect(normalizeDemoModel("second-model")).toBe("alpha/second-model");
|
||||
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "bravo",
|
||||
prefix: "bravo",
|
||||
workspaceDir: "/workspace/a",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(normalizeDemoModel()).toBe("bravo/demo-model");
|
||||
});
|
||||
|
||||
it("reflects manifest edits and state-dir changes on the next lookup", () => {
|
||||
const stateDirA = makeTempDir();
|
||||
const pluginDirA = path.join(stateDirA, "extensions", "normalizer");
|
||||
@@ -93,8 +232,8 @@ describe("manifest model id normalization", () => {
|
||||
|
||||
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
||||
|
||||
writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "bravo" });
|
||||
expect(normalizeDemoModel()).toBe("bravo/demo-model");
|
||||
writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "bravo-local" });
|
||||
expect(normalizeDemoModel()).toBe("bravo-local/demo-model");
|
||||
|
||||
const stateDirB = makeTempDir();
|
||||
const pluginDirB = path.join(stateDirB, "extensions", "normalizer");
|
||||
|
||||
@@ -1,123 +1,90 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginManifestModelIdNormalizationProvider } from "./manifest.js";
|
||||
import {
|
||||
loadPluginMetadataSnapshot,
|
||||
type PluginMetadataSnapshot,
|
||||
} from "./plugin-metadata-snapshot.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
type ManifestModelIdNormalizationLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
plugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
};
|
||||
|
||||
function normalizeTrimmedString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.map((entry) => normalizeTrimmedString(entry))
|
||||
.filter((entry): entry is string => entry !== undefined);
|
||||
}
|
||||
|
||||
function normalizePrefixRules(
|
||||
value: unknown,
|
||||
): PluginManifestModelIdNormalizationProvider["prefixWhenBareAfterAliasStartsWith"] {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const rules: NonNullable<
|
||||
PluginManifestModelIdNormalizationProvider["prefixWhenBareAfterAliasStartsWith"]
|
||||
> = [];
|
||||
for (const rawRule of value) {
|
||||
if (!isRecord(rawRule)) {
|
||||
continue;
|
||||
}
|
||||
const modelPrefix = normalizeTrimmedString(rawRule.modelPrefix);
|
||||
const prefix = normalizeTrimmedString(rawRule.prefix);
|
||||
if (modelPrefix && prefix) {
|
||||
rules.push({ modelPrefix, prefix });
|
||||
}
|
||||
}
|
||||
return rules.length > 0 ? rules : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelIdNormalizationPolicy(
|
||||
value: unknown,
|
||||
): PluginManifestModelIdNormalizationProvider | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const aliases: Record<string, string> = {};
|
||||
if (isRecord(value.aliases)) {
|
||||
for (const [aliasRaw, canonicalRaw] of Object.entries(value.aliases)) {
|
||||
const alias = normalizeLowercaseStringOrEmpty(aliasRaw);
|
||||
const canonical = normalizeTrimmedString(canonicalRaw);
|
||||
if (alias && canonical) {
|
||||
aliases[alias] = canonical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stripPrefixes = normalizeStringList(value.stripPrefixes);
|
||||
const prefixWhenBare = normalizeTrimmedString(value.prefixWhenBare);
|
||||
const prefixWhenBareAfterAliasStartsWith = normalizePrefixRules(
|
||||
value.prefixWhenBareAfterAliasStartsWith,
|
||||
);
|
||||
const policy = {
|
||||
...(Object.keys(aliases).length > 0 ? { aliases } : {}),
|
||||
...(stripPrefixes.length > 0 ? { stripPrefixes } : {}),
|
||||
...(prefixWhenBare ? { prefixWhenBare } : {}),
|
||||
...(prefixWhenBareAfterAliasStartsWith ? { prefixWhenBareAfterAliasStartsWith } : {}),
|
||||
} satisfies PluginManifestModelIdNormalizationProvider;
|
||||
|
||||
return Object.keys(policy).length > 0 ? policy : undefined;
|
||||
}
|
||||
|
||||
function readManifestModelIdNormalizationPolicies(
|
||||
manifest: Record<string, unknown>,
|
||||
): Array<[string, PluginManifestModelIdNormalizationProvider]> {
|
||||
const modelIdNormalization = manifest.modelIdNormalization;
|
||||
if (!isRecord(modelIdNormalization) || !isRecord(modelIdNormalization.providers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries: Array<[string, PluginManifestModelIdNormalizationProvider]> = [];
|
||||
for (const [providerRaw, rawPolicy] of Object.entries(modelIdNormalization.providers)) {
|
||||
const provider = normalizeLowercaseStringOrEmpty(providerRaw);
|
||||
const policy = normalizeModelIdNormalizationPolicy(rawPolicy);
|
||||
if (provider && policy) {
|
||||
entries.push([provider, policy]);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectManifestModelIdNormalizationPolicies(): Map<
|
||||
string,
|
||||
PluginManifestModelIdNormalizationProvider
|
||||
> {
|
||||
function collectManifestModelIdNormalizationPolicies(
|
||||
plugins: readonly Pick<PluginManifestRecord, "modelIdNormalization">[],
|
||||
): Map<string, PluginManifestModelIdNormalizationProvider> {
|
||||
const policies = new Map<string, PluginManifestModelIdNormalizationProvider>();
|
||||
for (const { manifest } of listOpenClawPluginManifestMetadata()) {
|
||||
for (const [provider, policy] of readManifestModelIdNormalizationPolicies(manifest)) {
|
||||
policies.set(provider, policy);
|
||||
for (const plugin of plugins) {
|
||||
for (const [provider, policy] of Object.entries(plugin.modelIdNormalization?.providers ?? {})) {
|
||||
policies.set(normalizeLowercaseStringOrEmpty(provider), policy);
|
||||
}
|
||||
}
|
||||
return policies;
|
||||
}
|
||||
|
||||
function loadManifestModelIdNormalizationPolicies(): Map<
|
||||
string,
|
||||
PluginManifestModelIdNormalizationProvider
|
||||
> {
|
||||
return collectManifestModelIdNormalizationPolicies();
|
||||
type ManifestModelIdNormalizationPolicyCache = {
|
||||
configFingerprint: string;
|
||||
policies: Map<string, PluginManifestModelIdNormalizationProvider>;
|
||||
};
|
||||
|
||||
let cachedPolicies: ManifestModelIdNormalizationPolicyCache | undefined;
|
||||
|
||||
function resolveMetadataSnapshotForPolicies(
|
||||
params: ManifestModelIdNormalizationLookupParams = {},
|
||||
): {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
cacheable: boolean;
|
||||
} {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
const current = getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
env,
|
||||
workspaceDir,
|
||||
});
|
||||
if (current) {
|
||||
return { snapshot: current, cacheable: true };
|
||||
}
|
||||
return {
|
||||
snapshot: loadPluginMetadataSnapshot({
|
||||
config: params.config ?? {},
|
||||
env,
|
||||
workspaceDir,
|
||||
}),
|
||||
cacheable: false,
|
||||
};
|
||||
}
|
||||
|
||||
function loadManifestModelIdNormalizationPolicies(
|
||||
params: ManifestModelIdNormalizationLookupParams = {},
|
||||
): Map<string, PluginManifestModelIdNormalizationProvider> {
|
||||
if (params.plugins) {
|
||||
return collectManifestModelIdNormalizationPolicies(params.plugins);
|
||||
}
|
||||
const { snapshot, cacheable } = resolveMetadataSnapshotForPolicies(params);
|
||||
const configFingerprint = snapshot.configFingerprint;
|
||||
if (cacheable && configFingerprint && cachedPolicies?.configFingerprint === configFingerprint) {
|
||||
return cachedPolicies.policies;
|
||||
}
|
||||
const policies = collectManifestModelIdNormalizationPolicies(snapshot.plugins);
|
||||
if (cacheable && configFingerprint) {
|
||||
cachedPolicies = { configFingerprint, policies };
|
||||
}
|
||||
return policies;
|
||||
}
|
||||
|
||||
function resolveManifestModelIdNormalizationPolicy(
|
||||
provider: string,
|
||||
params: ManifestModelIdNormalizationLookupParams = {},
|
||||
): PluginManifestModelIdNormalizationProvider | undefined {
|
||||
const providerId = normalizeLowercaseStringOrEmpty(provider);
|
||||
return loadManifestModelIdNormalizationPolicies().get(providerId);
|
||||
return loadManifestModelIdNormalizationPolicies(params).get(providerId);
|
||||
}
|
||||
|
||||
function hasProviderPrefix(modelId: string): boolean {
|
||||
@@ -130,12 +97,16 @@ function formatPrefixedModelId(prefix: string, modelId: string): string {
|
||||
|
||||
export function normalizeProviderModelIdWithManifest(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
plugins?: readonly Pick<PluginManifestRecord, "modelIdNormalization">[];
|
||||
context: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
};
|
||||
}): string | undefined {
|
||||
const policy = resolveManifestModelIdNormalizationPolicy(params.provider);
|
||||
const policy = resolveManifestModelIdNormalizationPolicy(params.provider, params);
|
||||
if (!policy) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearCurrentPluginMetadataSnapshot,
|
||||
resolvePluginMetadataControlPlaneFingerprint,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
const loadPluginRegistrySnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const loadPluginManifestRegistryForInstalledIndexMock = vi.hoisted(() => vi.fn());
|
||||
@@ -8,7 +18,8 @@ vi.mock("./plugin-registry.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./plugin-registry.js")>()),
|
||||
loadPluginRegistrySnapshot: loadPluginRegistrySnapshotMock,
|
||||
}));
|
||||
vi.mock("./manifest-registry-installed.js", () => ({
|
||||
vi.mock("./manifest-registry-installed.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./manifest-registry-installed.js")>()),
|
||||
loadPluginManifestRegistryForInstalledIndex: loadPluginManifestRegistryForInstalledIndexMock,
|
||||
}));
|
||||
vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||
@@ -16,11 +27,70 @@ vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
resetPluginRuntimeStateForTest();
|
||||
loadPluginRegistrySnapshotMock.mockReset();
|
||||
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
|
||||
loadPluginMetadataSnapshotMock.mockReset();
|
||||
});
|
||||
|
||||
function createCurrentSnapshot(params: {
|
||||
manifestHash: string;
|
||||
cliBackends: string[];
|
||||
workspaceDir?: string;
|
||||
}): PluginMetadataSnapshot {
|
||||
const policyHash = resolveInstalledPluginIndexPolicyHash({});
|
||||
const index: InstalledPluginIndex = {
|
||||
version: 1,
|
||||
hostContractVersion: "test-host",
|
||||
compatRegistryVersion: "test-compat",
|
||||
migrationVersion: 1,
|
||||
policyHash,
|
||||
generatedAtMs: 0,
|
||||
installRecords: {},
|
||||
plugins: [
|
||||
{
|
||||
pluginId: "openai",
|
||||
manifestPath: `/tmp/openai-${params.manifestHash}/openclaw.plugin.json`,
|
||||
manifestHash: params.manifestHash,
|
||||
source: `/tmp/openai-${params.manifestHash}/index.ts`,
|
||||
rootDir: `/tmp/openai-${params.manifestHash}`,
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
return {
|
||||
policyHash,
|
||||
configFingerprint: resolvePluginMetadataControlPlaneFingerprint(
|
||||
{},
|
||||
{
|
||||
env: process.env,
|
||||
index,
|
||||
policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
},
|
||||
),
|
||||
workspaceDir: params.workspaceDir,
|
||||
index,
|
||||
plugins: [
|
||||
{
|
||||
id: "openai",
|
||||
origin: "bundled",
|
||||
cliBackends: params.cliBackends,
|
||||
},
|
||||
],
|
||||
} as unknown as PluginMetadataSnapshot;
|
||||
}
|
||||
|
||||
describe("setup-registry runtime fallback", () => {
|
||||
it("uses bundled registry cliBackends when the setup-registry runtime is unavailable", async () => {
|
||||
loadPluginMetadataSnapshotMock.mockReturnValue({
|
||||
@@ -71,6 +141,90 @@ describe("setup-registry runtime fallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes bundled registry cliBackends when the current metadata snapshot changes", async () => {
|
||||
const { __testing, resolvePluginSetupCliBackendRuntime } =
|
||||
await import("./setup-registry.runtime.js");
|
||||
__testing.resetRuntimeState();
|
||||
__testing.setRuntimeModuleForTest(null);
|
||||
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "alpha",
|
||||
cliBackends: ["Codex-CLI"],
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "codex-cli" })).toEqual({
|
||||
pluginId: "openai",
|
||||
backend: { id: "Codex-CLI" },
|
||||
});
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "next-cli" })).toBeUndefined();
|
||||
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "bravo",
|
||||
cliBackends: ["Next-CLI"],
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "codex-cli" })).toBeUndefined();
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "next-cli" })).toEqual({
|
||||
pluginId: "openai",
|
||||
backend: { id: "Next-CLI" },
|
||||
});
|
||||
expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses workspace-scoped current metadata through the active plugin runtime", async () => {
|
||||
const { __testing, resolvePluginSetupCliBackendRuntime } =
|
||||
await import("./setup-registry.runtime.js");
|
||||
__testing.resetRuntimeState();
|
||||
__testing.setRuntimeModuleForTest(null);
|
||||
|
||||
setActivePluginRegistry(
|
||||
createEmptyPluginRegistry(),
|
||||
"workspace-a",
|
||||
"gateway-bindable",
|
||||
"/workspace/a",
|
||||
);
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "alpha",
|
||||
cliBackends: ["Codex-CLI"],
|
||||
workspaceDir: "/workspace/a",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "codex-cli", config: {} })).toEqual({
|
||||
pluginId: "openai",
|
||||
backend: { id: "Codex-CLI" },
|
||||
});
|
||||
expect(
|
||||
resolvePluginSetupCliBackendRuntime({ backend: "next-cli", config: {} }),
|
||||
).toBeUndefined();
|
||||
|
||||
setCurrentPluginMetadataSnapshot(
|
||||
createCurrentSnapshot({
|
||||
manifestHash: "bravo",
|
||||
cliBackends: ["Next-CLI"],
|
||||
workspaceDir: "/workspace/a",
|
||||
}),
|
||||
{ config: {}, env: process.env },
|
||||
);
|
||||
|
||||
expect(
|
||||
resolvePluginSetupCliBackendRuntime({ backend: "codex-cli", config: {} }),
|
||||
).toBeUndefined();
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "next-cli", config: {} })).toEqual({
|
||||
pluginId: "openai",
|
||||
backend: { id: "Next-CLI" },
|
||||
});
|
||||
expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves fail-closed setup lookup when the runtime module explicitly declines to resolve", async () => {
|
||||
loadPluginMetadataSnapshotMock.mockReturnValue({
|
||||
index: {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
|
||||
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
||||
import {
|
||||
loadPluginMetadataSnapshot,
|
||||
type PluginMetadataSnapshot,
|
||||
} from "./plugin-metadata-snapshot.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
|
||||
type SetupRegistryRuntimeModule = Pick<
|
||||
typeof import("./setup-registry.js"),
|
||||
@@ -15,23 +21,73 @@ type SetupCliBackendRuntimeEntry = {
|
||||
};
|
||||
};
|
||||
|
||||
type SetupCliBackendRuntimeLookupParams = {
|
||||
backend: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const SETUP_REGISTRY_RUNTIME_CANDIDATES = ["./setup-registry.js", "./setup-registry.ts"] as const;
|
||||
|
||||
type BundledSetupCliBackendCache = {
|
||||
configFingerprint: string;
|
||||
entries: SetupCliBackendRuntimeEntry[];
|
||||
};
|
||||
|
||||
let setupRegistryRuntimeModule: SetupRegistryRuntimeModule | null | undefined;
|
||||
let cachedBundledSetupCliBackends: BundledSetupCliBackendCache | undefined;
|
||||
|
||||
export const __testing = {
|
||||
resetRuntimeState(): void {
|
||||
setupRegistryRuntimeModule = undefined;
|
||||
cachedBundledSetupCliBackends = undefined;
|
||||
},
|
||||
setRuntimeModuleForTest(module: SetupRegistryRuntimeModule | null | undefined): void {
|
||||
setupRegistryRuntimeModule = module;
|
||||
},
|
||||
};
|
||||
|
||||
function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
|
||||
const snapshot = loadManifestMetadataSnapshot({ config: {}, env: process.env });
|
||||
return snapshot.plugins.flatMap((plugin) => {
|
||||
function resolveMetadataSnapshotForSetupCliBackends(
|
||||
params: Omit<SetupCliBackendRuntimeLookupParams, "backend"> = {},
|
||||
): {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
cacheable: boolean;
|
||||
} {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
const current = getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
env,
|
||||
workspaceDir,
|
||||
});
|
||||
if (current) {
|
||||
return { snapshot: current, cacheable: true };
|
||||
}
|
||||
return {
|
||||
snapshot: loadPluginMetadataSnapshot({
|
||||
config: params.config ?? {},
|
||||
env,
|
||||
workspaceDir,
|
||||
}),
|
||||
cacheable: false,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledSetupCliBackends(
|
||||
params: Omit<SetupCliBackendRuntimeLookupParams, "backend"> = {},
|
||||
): SetupCliBackendRuntimeEntry[] {
|
||||
const { snapshot, cacheable } = resolveMetadataSnapshotForSetupCliBackends(params);
|
||||
const configFingerprint = snapshot.configFingerprint;
|
||||
if (
|
||||
cacheable &&
|
||||
configFingerprint &&
|
||||
cachedBundledSetupCliBackends?.configFingerprint === configFingerprint
|
||||
) {
|
||||
return cachedBundledSetupCliBackends.entries;
|
||||
}
|
||||
const entries = snapshot.plugins.flatMap((plugin) => {
|
||||
if (plugin.origin !== "bundled" || !isInstalledPluginEnabled(snapshot.index, plugin.id)) {
|
||||
return [];
|
||||
}
|
||||
@@ -43,6 +99,10 @@ function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
|
||||
}) satisfies SetupCliBackendRuntimeEntry,
|
||||
);
|
||||
});
|
||||
if (cacheable && configFingerprint) {
|
||||
cachedBundledSetupCliBackends = { configFingerprint, entries };
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function loadSetupRegistryRuntime(): SetupRegistryRuntimeModule | null {
|
||||
@@ -57,16 +117,17 @@ function loadSetupRegistryRuntime(): SetupRegistryRuntimeModule | null {
|
||||
// Try source/runtime candidates in order.
|
||||
}
|
||||
}
|
||||
setupRegistryRuntimeModule = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolvePluginSetupCliBackendRuntime(params: { backend: string }) {
|
||||
export function resolvePluginSetupCliBackendRuntime(params: SetupCliBackendRuntimeLookupParams) {
|
||||
const normalized = normalizeProviderId(params.backend);
|
||||
const runtime = loadSetupRegistryRuntime();
|
||||
if (runtime !== null) {
|
||||
return runtime.resolvePluginSetupCliBackend(params);
|
||||
}
|
||||
return resolveBundledSetupCliBackends().find(
|
||||
return resolveBundledSetupCliBackends(params).find(
|
||||
(entry) => normalizeProviderId(entry.backend.id) === normalized,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user