























@@ -5,8 +5,10 @@ import {
55readConfigFileSnapshot,
66readConfigFileSnapshotForWrite,
77resolveConfigSnapshotHash,
8+validateConfigObjectRawWithPlugins,
89validateConfigObjectWithPlugins,
910} from "../../config/config.js";
11+import { createMergePatch, projectSourceOntoRuntimeShape } from "../../config/io.write-prepare.js";
1012import { formatConfigIssueLines } from "../../config/issue-format.js";
1113import { applyMergePatch } from "../../config/merge-patch.js";
1214import {
@@ -17,6 +19,7 @@ import {
1719import { loadGatewayRuntimeConfigSchema } from "../../config/runtime-schema.js";
1820import { lookupConfigSchema, type ConfigSchemaResponse } from "../../config/schema.js";
1921import type { ConfigValidationIssue, OpenClawConfig } from "../../config/types.openclaw.js";
22+import { isBuiltInModelProviderOverlayId } from "../../config/zod-schema.core.js";
2023import { formatErrorMessage } from "../../infra/errors.js";
2124import {
2225prepareSecretsRuntimeSnapshot,
@@ -189,12 +192,70 @@ function formatConfigOpenError(error: unknown): string {
189192return String(error);
190193}
191194195+function isRecord(value: unknown): value is Record<string, unknown> {
196+return Boolean(value) && typeof value === "object" && !Array.isArray(value);
197+}
198+199+function hasOwnRecordValue(value: unknown, key: string): boolean {
200+return isRecord(value) && Object.prototype.hasOwnProperty.call(value, key);
201+}
202+203+function stripBundledProviderRuntimeDefaults(params: {
204+candidate: unknown;
205+sourceConfig: unknown;
206+}): unknown {
207+if (!isRecord(params.candidate)) {
208+return params.candidate;
209+}
210+const models = params.candidate.models;
211+if (!isRecord(models) || !isRecord(models.providers)) {
212+return params.candidate;
213+}
214+const sourceModels = isRecord(params.sourceConfig) ? params.sourceConfig.models : undefined;
215+const sourceProviders = isRecord(sourceModels) ? sourceModels.providers : undefined;
216+217+let nextProviders: Record<string, unknown> | undefined;
218+for (const [providerId, provider] of Object.entries(models.providers)) {
219+if (!isBuiltInModelProviderOverlayId(providerId) || !isRecord(provider)) {
220+continue;
221+}
222+const sourceProvider = isRecord(sourceProviders) ? sourceProviders[providerId] : undefined;
223+let nextProvider: Record<string, unknown> | undefined;
224+if (provider.baseUrl === "" && !hasOwnRecordValue(sourceProvider, "baseUrl")) {
225+nextProvider = { ...provider };
226+delete nextProvider.baseUrl;
227+}
228+if (
229+Array.isArray(provider.models) &&
230+provider.models.length === 0 &&
231+!hasOwnRecordValue(sourceProvider, "models")
232+) {
233+nextProvider ??= { ...provider };
234+delete nextProvider.models;
235+}
236+if (nextProvider) {
237+nextProviders ??= { ...models.providers };
238+nextProviders[providerId] = nextProvider;
239+}
240+}
241+if (!nextProviders) {
242+return params.candidate;
243+}
244+return {
245+ ...params.candidate,
246+models: {
247+ ...models,
248+providers: nextProviders,
249+},
250+};
251+}
252+192253function parseValidateConfigFromRawOrRespond(
193254params: unknown,
194255requestName: string,
195256snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
196257respond: RespondFn,
197-): { config: OpenClawConfig; schema: ConfigSchemaResponse } | null {
258+): { config: OpenClawConfig; writeConfig: OpenClawConfig; schema: ConfigSchemaResponse } | null {
198259const rawValue = parseRawConfigOrRespond(params, requestName, respond);
199260if (!rawValue) {
200261return null;
@@ -214,7 +275,32 @@ function parseValidateConfigFromRawOrRespond(
214275);
215276return null;
216277}
217-const validated = validateConfigObjectWithPlugins(restored.result);
278+const projectedValidationCandidate = snapshot.valid
279+ ? applyMergePatch(
280+projectSourceOntoRuntimeShape(snapshot.resolved, snapshot.config),
281+createMergePatch(snapshot.config, restored.result),
282+)
283+ : restored.result;
284+const validationCandidate = stripBundledProviderRuntimeDefaults({
285+candidate: projectedValidationCandidate,
286+sourceConfig: snapshot.parsed,
287+});
288+const sourceValidated = validateConfigObjectRawWithPlugins(validationCandidate);
289+if (!sourceValidated.ok) {
290+respond(
291+false,
292+undefined,
293+errorShape(
294+ErrorCodes.INVALID_REQUEST,
295+summarizeConfigValidationIssues(sourceValidated.issues),
296+{
297+details: { issues: sourceValidated.issues },
298+},
299+),
300+);
301+return null;
302+}
303+const validated = validateConfigObjectWithPlugins(validationCandidate);
218304if (!validated.ok) {
219305respond(
220306false,
@@ -225,7 +311,11 @@ function parseValidateConfigFromRawOrRespond(
225311);
226312return null;
227313}
228-return { config: validated.config, schema };
314+return {
315+config: validated.config,
316+writeConfig: validationCandidate as OpenClawConfig,
317+ schema,
318+};
229319}
230320231321function summarizeConfigValidationIssues(issues: ReadonlyArray<ConfigValidationIssue>): string {
@@ -357,7 +447,7 @@ export const configHandlers: GatewayRequestHandlers = {
357447const writeResult = await commitGatewayConfigWrite({
358448 snapshot,
359449 writeOptions,
360-nextConfig: parsed.config,
450+nextConfig: parsed.writeConfig,
361451 context,
362452});
363453clearConfigSchemaResponseCache();
@@ -573,7 +663,7 @@ export const configHandlers: GatewayRequestHandlers = {
573663const writeResult = await commitGatewayConfigWrite({
574664 snapshot,
575665 writeOptions,
576-nextConfig: parsed.config,
666+nextConfig: parsed.writeConfig,
577667 context,
578668 disconnectSharedAuthClients,
579669});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。