mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 18:35:21 -06:00
b4d14d7848
* refactor(config): move model normalization to input owners * fix(config): merge canonical doctor model rows * fix(config): align touched model map identities * ci(env): lower production variable budget * chore: leave changelog to release automation
217 lines
7.0 KiB
TypeScript
217 lines
7.0 KiB
TypeScript
// Normalizes user-submitted model config at config mutation boundaries.
|
|
import {
|
|
normalizeConfiguredProviderCatalogModelId,
|
|
type ManifestModelIdNormalizationProvider,
|
|
} from "@openclaw/model-catalog-core/provider-model-id-normalization";
|
|
import { isRecord } from "../utils.js";
|
|
import { normalizeAgentModelMapForConfig, normalizeAgentModelRefForConfig } from "./model-input.js";
|
|
import type { OpenClawConfig } from "./types.openclaw.js";
|
|
|
|
const MODEL_SELECTION_KEYS = ["model", "imageModel", "voiceModel", "pdfModel"] as const;
|
|
const MEDIA_MODEL_KEYS = ["image", "video", "music"] as const;
|
|
|
|
function normalizeModelSelection(value: unknown): unknown {
|
|
if (typeof value === "string") {
|
|
return normalizeAgentModelRefForConfig(value);
|
|
}
|
|
if (!isRecord(value)) {
|
|
return value;
|
|
}
|
|
|
|
let next = value;
|
|
const assign = (key: string, candidate: unknown) => {
|
|
if (candidate === next[key]) {
|
|
return;
|
|
}
|
|
next = { ...next, [key]: candidate };
|
|
};
|
|
if (typeof value.primary === "string") {
|
|
assign("primary", normalizeAgentModelRefForConfig(value.primary));
|
|
}
|
|
if (Array.isArray(value.fallbacks)) {
|
|
const originalFallbacks = value.fallbacks;
|
|
const fallbacks = originalFallbacks.map((fallback) =>
|
|
typeof fallback === "string" ? normalizeAgentModelRefForConfig(fallback) : fallback,
|
|
);
|
|
if (fallbacks.some((fallback, index) => fallback !== originalFallbacks[index])) {
|
|
assign("fallbacks", fallbacks);
|
|
}
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function normalizeStringModelRef(value: unknown): unknown {
|
|
return typeof value === "string" ? normalizeAgentModelRefForConfig(value) : value;
|
|
}
|
|
|
|
function normalizeNestedModelField(
|
|
value: unknown,
|
|
key: string,
|
|
normalize: (candidate: unknown) => unknown,
|
|
): unknown {
|
|
if (!isRecord(value) || !Object.hasOwn(value, key)) {
|
|
return value;
|
|
}
|
|
const normalized = normalize(value[key]);
|
|
return normalized === value[key] ? value : { ...value, [key]: normalized };
|
|
}
|
|
|
|
function normalizeAgentModelScope(value: unknown): unknown {
|
|
if (!isRecord(value)) {
|
|
return value;
|
|
}
|
|
|
|
let next = value;
|
|
const assign = (key: string, candidate: unknown) => {
|
|
if (candidate === next[key]) {
|
|
return;
|
|
}
|
|
next = { ...next, [key]: candidate };
|
|
};
|
|
|
|
for (const key of MODEL_SELECTION_KEYS) {
|
|
if (Object.hasOwn(value, key)) {
|
|
assign(key, normalizeModelSelection(value[key]));
|
|
}
|
|
}
|
|
if (Object.hasOwn(value, "utilityModel")) {
|
|
assign("utilityModel", normalizeStringModelRef(value.utilityModel));
|
|
}
|
|
const originalMediaModels = value.mediaModels;
|
|
if (isRecord(originalMediaModels)) {
|
|
let mediaModelsChanged = false;
|
|
const mediaModels = { ...originalMediaModels };
|
|
for (const key of MEDIA_MODEL_KEYS) {
|
|
if (!Object.hasOwn(originalMediaModels, key)) {
|
|
continue;
|
|
}
|
|
const normalized = normalizeModelSelection(originalMediaModels[key]);
|
|
if (normalized !== mediaModels[key]) {
|
|
mediaModels[key] = normalized;
|
|
mediaModelsChanged = true;
|
|
}
|
|
}
|
|
if (mediaModelsChanged) {
|
|
assign("mediaModels", mediaModels);
|
|
}
|
|
}
|
|
assign("heartbeat", normalizeNestedModelField(value.heartbeat, "model", normalizeStringModelRef));
|
|
assign("subagents", normalizeNestedModelField(value.subagents, "model", normalizeModelSelection));
|
|
|
|
if (isRecord(value.compaction)) {
|
|
let compaction = normalizeNestedModelField(value.compaction, "model", normalizeStringModelRef);
|
|
if (isRecord(compaction)) {
|
|
const memoryFlush = normalizeNestedModelField(
|
|
compaction.memoryFlush,
|
|
"model",
|
|
normalizeStringModelRef,
|
|
);
|
|
if (memoryFlush !== compaction.memoryFlush) {
|
|
compaction = { ...compaction, memoryFlush };
|
|
}
|
|
}
|
|
assign("compaction", compaction);
|
|
}
|
|
if (isRecord(value.models)) {
|
|
assign("models", normalizeAgentModelMapForConfig(value.models));
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function normalizeAgentScopes(agents: unknown): unknown {
|
|
if (!isRecord(agents)) {
|
|
return agents;
|
|
}
|
|
let next = agents;
|
|
const assign = (key: string, candidate: unknown) => {
|
|
if (candidate === next[key]) {
|
|
return;
|
|
}
|
|
next = { ...next, [key]: candidate };
|
|
};
|
|
|
|
if (Object.hasOwn(agents, "defaults")) {
|
|
assign("defaults", normalizeAgentModelScope(agents.defaults));
|
|
}
|
|
if (isRecord(agents.entries)) {
|
|
let entriesChanged = false;
|
|
const entries = Object.fromEntries(
|
|
Object.entries(agents.entries).map(([agentId, entry]) => {
|
|
const normalized = normalizeAgentModelScope(entry);
|
|
entriesChanged ||= normalized !== entry;
|
|
return [agentId, normalized];
|
|
}),
|
|
);
|
|
if (entriesChanged) {
|
|
assign("entries", entries);
|
|
}
|
|
}
|
|
if (Array.isArray(agents.list)) {
|
|
const originalList = agents.list;
|
|
const list = originalList.map(normalizeAgentModelScope);
|
|
if (list.some((entry, index) => entry !== originalList[index])) {
|
|
assign("list", list);
|
|
}
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function normalizeProviderCatalogs(
|
|
models: unknown,
|
|
modelIdNormalizationPolicies?: ReadonlyMap<string, ManifestModelIdNormalizationProvider>,
|
|
): unknown {
|
|
if (!isRecord(models) || !isRecord(models.providers)) {
|
|
return models;
|
|
}
|
|
|
|
let providersChanged = false;
|
|
const providers = Object.fromEntries(
|
|
Object.entries(models.providers).map(([providerId, providerValue]) => {
|
|
if (!isRecord(providerValue) || !Array.isArray(providerValue.models)) {
|
|
return [providerId, providerValue];
|
|
}
|
|
const originalModels = providerValue.models;
|
|
const providerModels = originalModels.map((model) => {
|
|
if (!isRecord(model) || typeof model.id !== "string") {
|
|
return model;
|
|
}
|
|
const trimmed = model.id.trim();
|
|
if (!trimmed) {
|
|
return model;
|
|
}
|
|
const id = normalizeConfiguredProviderCatalogModelId(
|
|
providerId,
|
|
trimmed,
|
|
modelIdNormalizationPolicies,
|
|
);
|
|
return id === model.id ? model : { ...model, id };
|
|
});
|
|
if (providerModels.every((model, index) => model === originalModels[index])) {
|
|
return [providerId, providerValue];
|
|
}
|
|
providersChanged = true;
|
|
return [providerId, { ...providerValue, models: providerModels }];
|
|
}),
|
|
);
|
|
return providersChanged ? { ...models, providers } : models;
|
|
}
|
|
|
|
/** Canonicalize model refs submitted through a config mutation API before persistence. */
|
|
export function normalizeSubmittedConfigModelRefs(
|
|
cfg: OpenClawConfig,
|
|
modelIdNormalizationPolicies?: ReadonlyMap<string, ManifestModelIdNormalizationProvider>,
|
|
): OpenClawConfig {
|
|
let next = cfg;
|
|
const agents = normalizeAgentScopes(cfg.agents);
|
|
if (agents !== cfg.agents) {
|
|
next = { ...next, agents: agents as OpenClawConfig["agents"] };
|
|
}
|
|
const models = normalizeProviderCatalogs(cfg.models, modelIdNormalizationPolicies);
|
|
if (models !== cfg.models) {
|
|
next = { ...next, models: models as OpenClawConfig["models"] };
|
|
}
|
|
// tools.subagents owns only tool policy; model selection moved to
|
|
// agents.defaults/entries.*.subagents.model and the schema rejects the old key.
|
|
return next;
|
|
}
|