openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
423 lines (422 loc) • 19.8 kB
JavaScript
import { t as coerceErrorMessage } from "./error-coercion-CvJK4lmH.mjs";
import { n as formatConcreteConfigPath } from "./dot-path-CVD5Jolm.mjs";
import { n as containsEnvVarReference, r as resolveConfigEnvVars } from "./env-substitution-mBnQ8cYd.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { t as _usingCtx } from "./usingCtx-DwOBYOJf.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { T as tryResolveLegacyCompatibilityAgentId, a as resolveAgentDir, d as resolveAgentWorkspaceDir, g as resolveDefaultAgentId } from "./agent-scope-config-CDYpzA0r.mjs";
import "./session-key-C09ow4h0.mjs";
import { n as listAgentEntries, r as listAgentEntriesWithSource, t as hasAgentRosterProperty } from "./agent-roster-BXD1uMJD.mjs";
import { t as applyImplicitAgentRosterDefaults } from "./implicit-agent-roster-D39ONJLa.mjs";
import { r as DEFAULT_PROVIDER } from "./defaults-Jag53zwP.mjs";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BNjB2hED.mjs";
import { c as resolveAgentExplicitModelPrimary, u as resolveAgentModelFallbacksOverride } from "./agent-scope-CG17uXFn.mjs";
import { h as resolveConfiguredModelRef, i as buildModelAliasIndex, v as resolveModelRefFromString } from "./model-selection-shared-DcM6hdAC.mjs";
import { t as resolveDefaultModelForAgent } from "./model-selection-config-BdeHEviO.mjs";
//#region src/cli/config-model-validation.ts
function isPathPrefix(prefix, path) {
return prefix.length <= path.length && prefix.every((segment, index) => path[index] === segment);
}
function pathMayAffectTextModelRefs(path) {
if (path[0] !== "agents" || path.length === 1) return path[0] === "agents";
if (path[1] === "defaults") return path.length === 2 || path[2] === "model";
return path[1] === "entries" || path[1] === "list";
}
function collectTextModelConfigRefs(params) {
const stringModel = typeof params.model === "string";
const model = stringModel ? { primary: params.model } : params.model;
if (!model || typeof model !== "object" || Array.isArray(model)) return [];
const { primary, fallbacks } = model;
const refs = [];
if (typeof primary === "string") {
const value = primary.trim();
const authProfileId = splitTrailingAuthProfile(value).profile;
refs.push({
path: stringModel ? params.path : `${params.path}.primary`,
value,
...params.agentId ? { agentId: params.agentId } : {},
fallback: false,
...authProfileId ? { authProfileId } : {}
});
}
if (Array.isArray(fallbacks)) for (const [index, fallback] of fallbacks.entries()) {
if (typeof fallback !== "string") continue;
refs.push({
path: `${params.path}.fallbacks.${index}`,
value: fallback.trim(),
...params.agentId ? { agentId: params.agentId } : {},
fallback: true
});
}
return refs;
}
function collectTextModelRefs(config) {
const refs = collectTextModelConfigRefs({
model: config.agents?.defaults?.model,
path: "agents.defaults.model"
});
for (const { entry: agent, source } of listAgentEntriesWithSource(config)) {
const agentId = agent.id;
const agentPath = source.kind === "entries" ? `agents.entries.${source.key}` : `agents.list.${source.index}`;
refs.push(...collectTextModelConfigRefs({
model: agent.model,
path: `${agentPath}.model`,
agentId
}));
}
return refs;
}
function modelRefComparisonKey(ref) {
if (ref.agentId) {
const modelOffset = ref.path.indexOf(".model");
const relativePath = modelOffset >= 0 ? ref.path.slice(modelOffset + 1) : ref.path;
return `agent:${normalizeAgentId(ref.agentId)}:${relativePath}`;
}
return `path:${ref.path}`;
}
function inheritsDefaultModelRef(config, agentId, ref) {
return (ref.fallback ? resolveAgentModelFallbacksOverride : resolveAgentExplicitModelPrimary)(config, agentId) === void 0;
}
function collectTouchedTextModelRefs(params) {
const listedAgentEntries = listAgentEntriesWithSource(params.config);
const defaultPrimaryPath = [
"agents",
"defaults",
"model",
"primary"
];
const defaultPrimaryTouched = params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, defaultPrimaryPath) || isPathPrefix(defaultPrimaryPath, touchedPath));
const refs = collectTextModelRefs(params.config);
const previousRefs = params.previousConfig ? collectTextModelRefs(params.previousConfig) : void 0;
const previousRefsByIdentity = previousRefs ? new Map(previousRefs.map((ref) => [modelRefComparisonKey(ref), ref])) : void 0;
const previousDefaultAgentId = params.previousConfig ? tryResolveLegacyCompatibilityAgentId(params.previousConfig) : void 0;
const defaultPrimaryProviderChanged = defaultPrimaryTouched && (!previousRefs || previousDefaultAgentId === void 0 || resolveDefaultModelForAgent({ cfg: params.config }).provider !== resolveDefaultModelForAgent({
cfg: params.previousConfig,
agentId: previousDefaultAgentId
}).provider);
const touchedRefs = refs.filter((ref) => {
if (ref.fallback && defaultPrimaryProviderChanged) {
const previousRef = previousRefsByIdentity?.get(modelRefComparisonKey(ref));
const nextResolved = resolveFallbackRef(params.config, ref.value)?.ref;
const previousResolved = params.previousConfig && previousRef ? resolveFallbackRef(params.previousConfig, previousRef.value)?.ref : void 0;
if (!nextResolved || !previousResolved || nextResolved.provider !== previousResolved.provider || nextResolved.model !== previousResolved.model) {
ref.dependency = true;
return true;
}
}
const refPath = ref.path.split(".");
const touched = params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, refPath) || isPathPrefix(refPath, touchedPath));
if (!touched || !previousRefsByIdentity) return touched;
const previousRef = previousRefsByIdentity.get(modelRefComparisonKey(ref));
const ownerChanged = previousRef?.agentId !== ref.agentId;
if (ownerChanged) ref.dependency = true;
return previousRef?.value !== ref.value || ownerChanged;
});
const defaultRefs = refs.filter((ref) => ref.agentId === void 0);
if (defaultRefs.length === 0) return touchedRefs;
for (const { entry, source } of listedAgentEntries) {
const agentId = entry.id;
const agentEntryPath = [
"agents",
source.kind,
source.kind === "entries" ? source.key : String(source.index)
];
if (!params.touchedPaths.some((touchedPath) => isPathPrefix(touchedPath, agentEntryPath) || isPathPrefix(agentEntryPath, touchedPath))) continue;
for (const defaultRef of defaultRefs) {
const inherits = inheritsDefaultModelRef(params.config, agentId, defaultRef);
const previouslyInherited = (params.previousConfig ? listAgentEntries(params.previousConfig) : []).some((previousEntry) => normalizeAgentId(previousEntry.id) === normalizeAgentId(agentId)) && params.previousConfig && inheritsDefaultModelRef(params.previousConfig, agentId, defaultRef);
if (inherits && !previouslyInherited) touchedRefs.push({
...defaultRef,
agentId,
dependency: true
});
}
}
return touchedRefs;
}
function resolveCanonicalPrimaryRef(config, value, manifestPlugins, agentId) {
const validationConfig = {
...config,
agents: {
...config.agents,
defaults: {
...config.agents?.defaults,
model: value
}
}
};
const resolved = resolveConfiguredModelRef({
cfg: validationConfig,
agentId,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: "",
allowPluginNormalization: true,
manifestPlugins
});
return resolved.model ? resolved : void 0;
}
function resolveFallbackRef(config, value, manifestPlugins, agentId) {
const defaultProvider = resolveDefaultModelForAgent({
cfg: config,
agentId,
manifestPlugins
}).provider;
return resolveModelRefFromString({
cfg: config,
agentId,
raw: value,
defaultProvider,
aliasIndex: buildModelAliasIndex({
cfg: config,
agentId,
defaultProvider,
allowPluginNormalization: true,
manifestPlugins
}),
allowPluginNormalization: true,
manifestPlugins
});
}
function resolveCanonicalModelRef(config, ref, manifestPlugins) {
const agentId = ref.agentId ?? tryResolveLegacyCompatibilityAgentId(config);
return ref.fallback ? resolveFallbackRef(config, ref.value, manifestPlugins, agentId)?.ref : resolveCanonicalPrimaryRef(config, ref.value, manifestPlugins, agentId);
}
function scopedModelRefComparisonKey(config, ref) {
return modelRefComparisonKey({
...ref,
agentId: ref.agentId ?? tryResolveLegacyCompatibilityAgentId(config)
});
}
function hasUnresolvedInheritedFallbackProvider(config, ref, unresolvedPaths) {
if (!ref.fallback || ref.value.includes("/")) return false;
const agentId = ref.agentId ?? tryResolveLegacyCompatibilityAgentId(config);
const primaries = collectTextModelRefs(config).filter((candidate) => !candidate.fallback);
const primaryRef = primaries.find((candidate) => candidate.agentId === agentId) ?? primaries.find((candidate) => candidate.agentId === void 0);
if (!primaryRef || !unresolvedPaths.has(formatConcreteConfigPath(primaryRef.path.split("."), config))) return false;
const primaryModel = splitTrailingAuthProfile(primaryRef.value).model;
const slash = primaryModel.indexOf("/");
const provider = slash > 0 ? primaryModel.slice(0, slash) : primaryModel;
const fallback = resolveFallbackRef(config, ref.value, void 0, agentId);
return Boolean(fallback && !fallback.alias && containsEnvVarReference(provider));
}
function expandInheritedDefaultRefs(config, refs) {
const agentEntries = listAgentEntries(config);
const defaultAgentId = tryResolveLegacyCompatibilityAgentId(config);
const expanded = [];
const seen = /* @__PURE__ */ new Set();
const push = (ref) => {
const key = `${ref.path}\u0000${ref.agentId ?? ""}`;
if (!seen.has(key)) {
seen.add(key);
expanded.push(ref);
}
};
for (const ref of refs) {
if (ref.agentId !== void 0) {
push(ref);
continue;
}
if (defaultAgentId) {
if (!agentEntries.some((entry) => normalizeAgentId(entry.id) === normalizeAgentId(defaultAgentId)) || inheritsDefaultModelRef(config, defaultAgentId, ref)) push(ref);
}
for (const { id: agentId } of agentEntries) {
if (defaultAgentId && normalizeAgentId(agentId) === normalizeAgentId(defaultAgentId)) continue;
if (inheritsDefaultModelRef(config, agentId, ref)) push({
...ref,
agentId
});
}
}
return expanded;
}
function validateModelRefSyntax(config, ref, unresolvedPaths) {
if (!ref.value) return "Model reference is empty";
const configPath = formatConcreteConfigPath(ref.path.split("."), config);
if (unresolvedPaths.has(configPath)) return "Model reference contains an unresolved environment variable";
return resolveCanonicalModelRef(config, ref) ? void 0 : "Invalid model reference or configured model alias target";
}
async function createRuntimeModelRefResolver() {
const modelSelection = await import("./model-selection-BamC1Dho.mjs");
const loadModelModules = () => Promise.all([import("./model-BFYl1bVF.mjs"), import("./prepared-model-runtime-Dac6Df6C.mjs")]);
let modelModules;
return async ({ config, ref }) => {
try {
var _usingCtx$1 = _usingCtx();
let resolvedRef = resolveCanonicalModelRef(config, ref);
if (!resolvedRef) return `Unknown model: ${ref.value}`;
if (modelSelection.isCliProvider(resolvedRef.provider, config)) return;
const targetAgentId = ref.agentId ?? tryResolveLegacyCompatibilityAgentId(config) ?? resolveDefaultAgentId(config);
const agentDir = resolveAgentDir(config, targetAgentId);
const workspaceDir = resolveAgentWorkspaceDir(config, targetAgentId);
const [modelRuntime, preparedRuntime] = await (modelModules ??= loadModelModules());
const lease = _usingCtx$1.a(await preparedRuntime.acquireReadOnlyPreparedModelRuntime({
agentId: targetAgentId,
agentDir,
config,
workspaceDir,
loadRuntimePlugins: true
}, { deriveRuntimePluginSelections: ({ config: admittedConfig, metadataSnapshot }) => {
resolvedRef = resolveCanonicalModelRef(admittedConfig, ref, metadataSnapshot);
if (!resolvedRef) return [];
const { provider, model } = resolvedRef;
return [{
provider,
modelId: model,
agentId: targetAgentId
}];
} }));
if (!resolvedRef) return `Unknown model: ${ref.value}`;
const { provider, model } = resolvedRef;
const stores = lease.snapshot.createStores();
const resolution = await modelRuntime.resolveModelAsync(provider, model, agentDir, config, {
...stores,
modelIdSource: "selected",
agentId: targetAgentId,
allowBundledStaticCatalogFallback: true,
...ref.authProfileId ? { authProfileId: ref.authProfileId } : {},
preparedModelRuntime: lease.snapshot,
workspaceDir
});
return resolution.model ? void 0 : resolution.error ?? `Unknown model: ${provider}/${model}`;
} catch (_) {
_usingCtx$1.e = _;
} finally {
await _usingCtx$1.d();
}
};
}
function formatModelRefError(ref, error, authoredValue = ref.value, options) {
const safeError = options?.suppressDetail || authoredValue !== ref.value ? "Unable to resolve authored model reference" : error;
const detail = safeError.endsWith(".") ? safeError : `${safeError}.`;
return `Cannot set model reference "${authoredValue}" at ${ref.path}: ${detail} Run ${formatCliCommand("openclaw models list")} to list available models.`;
}
function materializeValidationRoster(config) {
return hasAgentRosterProperty(config) ? config : applyImplicitAgentRosterDefaults(config);
}
/** Checks authored mutations before admission, preserving legacy roster alias paths. */
async function checkTouchedTextModelRefs(params) {
const touchedPaths = params.touchedPaths;
const modelDependenciesTouched = touchedPaths.some((path) => path[0] === "env" || path[0] === "agents" && (path.length === 1 || path[1] === "defaults" && (path.length === 2 || path[2] === "models" || path[2] === "model" && (path.length === 3 || path[3] === "primary")) || (path[1] === "entries" || path[1] === "list") && (path.length <= 3 || path[3] === "models" || path[3] === "model" && (path.length === 4 || path[4] === "primary"))));
if (!modelDependenciesTouched && !touchedPaths.some(pathMayAffectTextModelRefs)) return {
refsChecked: 0,
refsTotal: 0,
errors: []
};
const config = materializeValidationRoster(params.config);
const previousConfig = params.previousConfig ? materializeValidationRoster(params.previousConfig) : void 0;
const authoredRefs = collectTouchedTextModelRefs({
...params,
config,
previousConfig,
touchedPaths
});
const authoredValuesByPath = new Map(collectTextModelRefs(params.config).map((ref) => [ref.path, ref.value]));
const previousAuthoredValuesByPath = new Map(collectTextModelRefs(params.previousConfig ?? {}).map((ref) => [ref.path, ref.value]));
let validationConfig;
let validationPreviousConfig;
const unresolvedPaths = /* @__PURE__ */ new Set();
try {
const env = params.env ?? process.env;
validationConfig = resolveConfigEnvVars(params.config, env, { onMissing: ({ configPath }) => unresolvedPaths.add(configPath) });
validationPreviousConfig = params.previousConfig ? resolveConfigEnvVars(params.previousConfig, params.previousEnv ?? env, { onMissing: () => {} }) : void 0;
} catch (cause) {
const detail = coerceErrorMessage(cause);
return {
refsChecked: 0,
refsTotal: authoredRefs.length,
errors: [`Unable to validate changed model references before writing: ${detail}`]
};
}
const validationRefsByPath = new Map(collectTextModelRefs(validationConfig).map((ref) => [ref.path, ref]));
const modelEnvWasExpanded = [...authoredValuesByPath].some(([path, value]) => validationRefsByPath.get(path)?.value !== value);
const formatError = (ref, error) => {
const redactDependency = Boolean(params.redactDependencyValues && ref.dependency);
return formatModelRefError(ref, error, redactDependency ? "<configured model reference>" : authoredValuesByPath.get(ref.path), { suppressDetail: modelEnvWasExpanded || redactDependency });
};
const validationRosterConfig = materializeValidationRoster(validationConfig);
const validationPreviousRosterConfig = validationPreviousConfig ? materializeValidationRoster(validationPreviousConfig) : void 0;
const refsByKey = new Map(collectTouchedTextModelRefs({
config: validationRosterConfig,
previousConfig: validationPreviousRosterConfig,
touchedPaths
}).map((ref) => [modelRefComparisonKey(ref), ref]));
for (const authoredRef of authoredRefs) {
if (authoredRef.dependency && previousAuthoredValuesByPath.get(authoredRef.path) === authoredRef.value) continue;
const validationRef = validationRefsByPath.get(authoredRef.path);
if (!validationRef) continue;
const key = modelRefComparisonKey(validationRef);
const expandedRef = refsByKey.get(key);
refsByKey.set(key, {
...validationRef,
...authoredRef.dependency || expandedRef?.dependency ? { dependency: true } : {}
});
}
const refsByOwner = new Map(expandInheritedDefaultRefs(validationRosterConfig, [...refsByKey.values()]).map((ref) => [scopedModelRefComparisonKey(validationRosterConfig, ref), ref]));
if (modelDependenciesTouched && validationPreviousRosterConfig) {
const previousRefsByOwner = new Map(expandInheritedDefaultRefs(validationPreviousRosterConfig, collectTextModelRefs(validationPreviousRosterConfig)).map((ref) => [scopedModelRefComparisonKey(validationPreviousRosterConfig, ref), ref]));
for (const ref of expandInheritedDefaultRefs(validationRosterConfig, collectTextModelRefs(validationRosterConfig))) {
const key = scopedModelRefComparisonKey(validationRosterConfig, ref);
if (refsByOwner.has(key)) continue;
const previousRef = previousRefsByOwner.get(key);
const previousResolved = previousRef ? resolveCanonicalModelRef(validationPreviousRosterConfig, previousRef) : void 0;
const nextResolved = resolveCanonicalModelRef(validationRosterConfig, ref);
if (previousRef?.value !== ref.value || previousResolved?.provider !== nextResolved?.provider || previousResolved?.model !== nextResolved?.model) refsByOwner.set(key, {
...ref,
dependency: true
});
}
}
const refs = [...refsByOwner.values()];
if (refs.length === 0) return {
refsChecked: 0,
refsTotal: 0,
errors: []
};
const validatedRefs = refs.filter((ref) => !hasUnresolvedInheritedFallbackProvider(config, ref, unresolvedPaths)).map((ref) => ({
ref,
error: validateModelRefSyntax(validationConfig, ref, unresolvedPaths)
}));
const syntaxFailures = validatedRefs.filter((entry) => Boolean(entry.error));
const refsToResolve = validatedRefs.filter((entry) => !entry.error).map((entry) => entry.ref);
const errors = syntaxFailures.map(({ ref, error }) => formatError(ref, error));
if (refsToResolve.length === 0) return {
refsChecked: syntaxFailures.length,
refsTotal: refs.length,
errors
};
let resolveModelRef = params.resolveModelRef;
if (!resolveModelRef) try {
resolveModelRef = await (params.createModelRefResolver ?? createRuntimeModelRefResolver)();
} catch (cause) {
const detail = modelEnvWasExpanded || Boolean(params.redactDependencyValues && refs.some((ref) => ref.dependency)) ? "model resolver setup failed" : coerceErrorMessage(cause);
return {
refsChecked: syntaxFailures.length,
refsTotal: refs.length,
errors: [...errors, `Unable to validate changed model references before writing: ${detail}`]
};
}
let refsChecked = syntaxFailures.length;
for (const ref of refsToResolve) {
let error;
try {
error = await resolveModelRef({
config: validationConfig,
ref
});
refsChecked += 1;
} catch (cause) {
const detail = coerceErrorMessage(cause);
errors.push(formatError(ref, `Unable to validate model reference: ${detail}`));
continue;
}
if (!error) continue;
errors.push(formatError(ref, error));
}
return {
refsChecked,
refsTotal: refs.length,
errors
};
}
//#endregion
export { checkTouchedTextModelRefs };