From 0abedd546a38ce2cb3e6bb20207875ddc53155d0 Mon Sep 17 00:00:00 2001 From: NianJiu <3235467914@qq.com> Date: Sun, 24 May 2026 10:48:05 +0800 Subject: [PATCH] fix(models): preserve source snapshots for SecretRef providers * fix(models): preserve source snapshots for SecretRef providers * docs: add models SecretRef changelog entry --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + ...els-config.runtime-source-snapshot.test.ts | 52 +++++++++++++++++++ src/commands/models/load-config.test.ts | 2 +- src/commands/models/load-config.ts | 6 +-- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 655c614c612..f793c74fb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai - Discord/voice: recover stale realtime playback state when Discord stream-close/player-idle events do not arrive, and keep generated runtime plugin aliases available after postbuild rewrites. - Discord/voice: keep realtime playback running when meeting notes attaches to an existing voice session or a realtime consult starts, and route realtime user transcripts into meeting notes. - Config/secrets: preflight active runtime SecretRefs before root and include config writes persist, and roll back unchanged file/env state when post-write refresh fails. Fixes #46531. (#84454) Thanks @samzong. +- CLI/models: preserve SecretRef-backed custom provider `apiKey` markers when `models status` regenerates `models.json`, avoiding resolved plaintext secrets on disk. Fixes #84632. (#84658) Thanks @NianJiuZst. - WebChat: keep the run-complete indicator in progress until deferred history replay renders the assistant reply, so Done no longer appears before response text. (#85374) Thanks @neeravmakwana. - Agents/tools: give timed-out or cancelled process trees a bounded SIGTERM cleanup window before SIGKILL while preserving tree-aware cancellation. Fixes #66399. (#85865) Thanks @IWhatsskill. - Agents/compaction: skip agent-harness preflight for provider-owned CLI runtime sessions so over-threshold Claude CLI sessions continue through normal compaction instead of failing on a missing harness. Fixes #84857. (#84878) Thanks @zhangguiping-xydt. diff --git a/src/agents/models-config.runtime-source-snapshot.test.ts b/src/agents/models-config.runtime-source-snapshot.test.ts index d3d32df5beb..f5a3ad06e6f 100644 --- a/src/agents/models-config.runtime-source-snapshot.test.ts +++ b/src/agents/models-config.runtime-source-snapshot.test.ts @@ -100,6 +100,40 @@ function createOpenAiApiKeyRuntimeConfig(): OpenClawConfig { }; } +function createCustomProviderApiKeySourceConfig(): OpenClawConfig { + return { + models: { + providers: { + litellm: { + baseUrl: "https://litellm.example/v1", + apiKey: { + source: "env", + provider: "default", + id: "OPENCLAW_MODEL_LITELLM_API_KEY", // pragma: allowlist secret + }, + api: "openai-completions" as const, + models: [], + }, + }, + }, + }; +} + +function createCustomProviderApiKeyRuntimeConfig(): OpenClawConfig { + return { + models: { + providers: { + litellm: { + baseUrl: "https://litellm.example/v1", + apiKey: "sk-litellm-runtime-secret", // pragma: allowlist secret + api: "openai-completions" as const, + models: [], + }, + }, + }, + }; +} + function createOpenAiHeaderSourceConfig(): OpenClawConfig { return { models: { @@ -280,6 +314,24 @@ describe("models-config runtime source snapshot", () => { }); }); + it("preserves source markers for custom-provider api keys after models status secret resolution", async () => { + const agentDir = await fixtureSuite.createCaseDir("agent"); + await withTempEnv(MODELS_CONFIG_IMPLICIT_ENV_VARS, async () => { + unsetEnv(MODELS_CONFIG_IMPLICIT_ENV_VARS); + const sourceConfig = createCustomProviderApiKeySourceConfig(); + const runtimeConfig = createCustomProviderApiKeyRuntimeConfig(); + + try { + setRuntimeConfigSnapshot(runtimeConfig, sourceConfig); + await ensureOpenClawModelsJson(runtimeConfig, agentDir); + await expectGeneratedProviderApiKey(agentDir, "litellm", "OPENCLAW_MODEL_LITELLM_API_KEY"); // pragma: allowlist secret + } finally { + clearRuntimeConfigSnapshot(); + clearConfigCache(); + } + }); + }); + it("invalidates cached readiness when projected config changes under the same runtime snapshot", async () => { const agentDir = await fixtureSuite.createCaseDir("agent"); await withTempEnv(MODELS_CONFIG_IMPLICIT_ENV_VARS, async () => { diff --git a/src/commands/models/load-config.test.ts b/src/commands/models/load-config.test.ts index 9f17b94e1dc..69578e74e18 100644 --- a/src/commands/models/load-config.test.ts +++ b/src/commands/models/load-config.test.ts @@ -99,6 +99,6 @@ describe("models load-config", () => { const result = await loadModelsConfigWithSource({ commandName: "models list" }); expect(result.sourceConfig).toBe(runtimeConfig); - expect(mocks.setRuntimeConfigSnapshot).toHaveBeenCalledWith(resolvedConfig); + expect(mocks.setRuntimeConfigSnapshot).toHaveBeenCalledWith(resolvedConfig, runtimeConfig); }); }); diff --git a/src/commands/models/load-config.ts b/src/commands/models/load-config.ts index 66bee90e157..e3b9b5a1f93 100644 --- a/src/commands/models/load-config.ts +++ b/src/commands/models/load-config.ts @@ -27,11 +27,7 @@ export async function loadModelsConfigWithSource(params: { targetIds: getModelsCommandSecretTargetIds(), runtime: params.runtime, }); - if (pinnedSourceConfig) { - setRuntimeConfigSnapshot(resolvedConfig, sourceConfig); - } else { - setRuntimeConfigSnapshot(resolvedConfig); - } + setRuntimeConfigSnapshot(resolvedConfig, sourceConfig); return { sourceConfig, resolvedConfig,