diff --git a/docs/refactor/cleanup.md b/docs/refactor/cleanup.md index 83b1fb47c5b..f825164c278 100644 --- a/docs/refactor/cleanup.md +++ b/docs/refactor/cleanup.md @@ -3,5 +3,5 @@ - [x] Extract `models list` row/supplement helpers. - [x] Split `models list` forward-compat tests by concern. - [x] Extract provider transport normalization from `pi-embedded-runner/model.ts`. -- [ ] Split `ensureOpenClawModelsJson()` into planning + IO layers. +- [x] Split `ensureOpenClawModelsJson()` into planning + IO layers. - [ ] Split provider discovery helpers out of `models-config.providers.ts`. diff --git a/src/agents/models-config.plan.ts b/src/agents/models-config.plan.ts new file mode 100644 index 00000000000..40777c2cd0d --- /dev/null +++ b/src/agents/models-config.plan.ts @@ -0,0 +1,128 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { isRecord } from "../utils.js"; +import { + mergeProviders, + mergeWithExistingProviderSecrets, + type ExistingProviderConfig, +} from "./models-config.merge.js"; +import { + normalizeProviders, + resolveImplicitProviders, + type ProviderConfig, +} from "./models-config.providers.js"; + +type ModelsConfig = NonNullable; + +export type ModelsJsonPlan = + | { + action: "skip"; + } + | { + action: "noop"; + } + | { + action: "write"; + contents: string; + }; + +async function resolveProvidersForModelsJson(params: { + cfg: OpenClawConfig; + agentDir: string; + env: NodeJS.ProcessEnv; +}): Promise> { + const { cfg, agentDir, env } = params; + const explicitProviders = cfg.models?.providers ?? {}; + const implicitProviders = await resolveImplicitProviders({ + agentDir, + config: cfg, + env, + explicitProviders, + }); + return mergeProviders({ + implicit: implicitProviders, + explicit: explicitProviders, + }); +} + +function resolveExplicitBaseUrlProviders( + providers: OpenClawConfig["models"] | undefined, +): ReadonlySet { + return new Set( + Object.entries(providers?.providers ?? {}) + .map(([key, provider]) => [key.trim(), provider] as const) + .filter( + ([key, provider]) => + Boolean(key) && typeof provider?.baseUrl === "string" && provider.baseUrl.trim(), + ) + .map(([key]) => key), + ); +} + +async function resolveProvidersForMode(params: { + mode: NonNullable; + existingParsed: unknown; + providers: Record; + secretRefManagedProviders: ReadonlySet; + explicitBaseUrlProviders: ReadonlySet; +}): Promise> { + if (params.mode !== "merge") { + return params.providers; + } + const existing = params.existingParsed; + if (!isRecord(existing) || !isRecord(existing.providers)) { + return params.providers; + } + const existingProviders = existing.providers as Record< + string, + NonNullable[string] + >; + return mergeWithExistingProviderSecrets({ + nextProviders: params.providers, + existingProviders: existingProviders as Record, + secretRefManagedProviders: params.secretRefManagedProviders, + explicitBaseUrlProviders: params.explicitBaseUrlProviders, + }); +} + +export async function planOpenClawModelsJson(params: { + cfg: OpenClawConfig; + agentDir: string; + env: NodeJS.ProcessEnv; + existingRaw: string; + existingParsed: unknown; +}): Promise { + const { cfg, agentDir, env } = params; + const providers = await resolveProvidersForModelsJson({ cfg, agentDir, env }); + + if (Object.keys(providers).length === 0) { + return { action: "skip" }; + } + + const mode = cfg.models?.mode ?? "merge"; + const secretRefManagedProviders = new Set(); + const normalizedProviders = + normalizeProviders({ + providers, + agentDir, + env, + secretDefaults: cfg.secrets?.defaults, + secretRefManagedProviders, + }) ?? providers; + const mergedProviders = await resolveProvidersForMode({ + mode, + existingParsed: params.existingParsed, + providers: normalizedProviders, + secretRefManagedProviders, + explicitBaseUrlProviders: resolveExplicitBaseUrlProviders(cfg.models), + }); + const nextContents = `${JSON.stringify({ providers: mergedProviders }, null, 2)}\n`; + + if (params.existingRaw === nextContents) { + return { action: "noop" }; + } + + return { + action: "write", + contents: nextContents, + }; +} diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 8fa237fcaf3..b9b8a7316d3 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -7,22 +7,9 @@ import { loadConfig, } from "../config/config.js"; import { createConfigRuntimeEnv } from "../config/env-vars.js"; -import { isRecord } from "../utils.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js"; -import { - mergeProviders, - mergeWithExistingProviderSecrets, - type ExistingProviderConfig, -} from "./models-config.merge.js"; -import { - normalizeProviders, - type ProviderConfig, - resolveImplicitProviders, -} from "./models-config.providers.js"; +import { planOpenClawModelsJson } from "./models-config.plan.js"; -type ModelsConfig = NonNullable; - -const DEFAULT_MODE: NonNullable = "merge"; const MODELS_JSON_WRITE_LOCKS = new Map>(); async function readExistingModelsFile(pathname: string): Promise<{ @@ -43,52 +30,6 @@ async function readExistingModelsFile(pathname: string): Promise<{ } } -async function resolveProvidersForModelsJson(params: { - cfg: OpenClawConfig; - agentDir: string; - env: NodeJS.ProcessEnv; -}): Promise> { - const { cfg, agentDir, env } = params; - const explicitProviders = cfg.models?.providers ?? {}; - const implicitProviders = await resolveImplicitProviders({ - agentDir, - config: cfg, - env, - explicitProviders, - }); - const providers: Record = mergeProviders({ - implicit: implicitProviders, - explicit: explicitProviders, - }); - return providers; -} - -async function resolveProvidersForMode(params: { - mode: NonNullable; - existingParsed: unknown; - providers: Record; - secretRefManagedProviders: ReadonlySet; - explicitBaseUrlProviders: ReadonlySet; -}): Promise> { - if (params.mode !== "merge") { - return params.providers; - } - const existing = params.existingParsed; - if (!isRecord(existing) || !isRecord(existing.providers)) { - return params.providers; - } - const existingProviders = existing.providers as Record< - string, - NonNullable[string] - >; - return mergeWithExistingProviderSecrets({ - nextProviders: params.providers, - existingProviders: existingProviders as Record, - secretRefManagedProviders: params.secretRefManagedProviders, - explicitBaseUrlProviders: params.explicitBaseUrlProviders, - }); -} - async function ensureModelsFileMode(pathname: string): Promise { await fs.chmod(pathname, 0o600).catch(() => { // best-effort @@ -147,50 +88,26 @@ export async function ensureOpenClawModelsJson( // Ensure config env vars (e.g. AWS_PROFILE, AWS_ACCESS_KEY_ID) are // are available to provider discovery without mutating process.env. const env = createConfigRuntimeEnv(cfg); + const existingModelsFile = await readExistingModelsFile(targetPath); + const plan = await planOpenClawModelsJson({ + cfg, + agentDir, + env, + existingRaw: existingModelsFile.raw, + existingParsed: existingModelsFile.parsed, + }); - const providers = await resolveProvidersForModelsJson({ cfg, agentDir, env }); - - if (Object.keys(providers).length === 0) { + if (plan.action === "skip") { return { agentDir, wrote: false }; } - const mode = cfg.models?.mode ?? DEFAULT_MODE; - const secretRefManagedProviders = new Set(); - const explicitBaseUrlProviders = new Set( - Object.entries(cfg.models?.providers ?? {}) - .map(([key, provider]) => [key.trim(), provider] as const) - .filter( - ([key, provider]) => - Boolean(key) && typeof provider?.baseUrl === "string" && provider.baseUrl.trim(), - ) - .map(([key]) => key), - ); - - const normalizedProviders = - normalizeProviders({ - providers, - agentDir, - env, - secretDefaults: cfg.secrets?.defaults, - secretRefManagedProviders, - }) ?? providers; - const existingModelsFile = await readExistingModelsFile(targetPath); - const mergedProviders = await resolveProvidersForMode({ - mode, - existingParsed: existingModelsFile.parsed, - providers: normalizedProviders, - secretRefManagedProviders, - explicitBaseUrlProviders, - }); - const next = `${JSON.stringify({ providers: mergedProviders }, null, 2)}\n`; - - if (existingModelsFile.raw === next) { + if (plan.action === "noop") { await ensureModelsFileMode(targetPath); return { agentDir, wrote: false }; } await fs.mkdir(agentDir, { recursive: true, mode: 0o700 }); - await writeModelsFileAtomic(targetPath, next); + await writeModelsFileAtomic(targetPath, plan.contents); await ensureModelsFileMode(targetPath); return { agentDir, wrote: true }; });