refactor(models): split models.json planning from writes

This commit is contained in:
Peter Steinberger
2026-03-09 00:21:37 +00:00
parent 24b53fcf47
commit c29b098744
3 changed files with 141 additions and 96 deletions

View File

@@ -3,5 +3,5 @@
- [x] Extract `models list` row/supplement helpers. - [x] Extract `models list` row/supplement helpers.
- [x] Split `models list` forward-compat tests by concern. - [x] Split `models list` forward-compat tests by concern.
- [x] Extract provider transport normalization from `pi-embedded-runner/model.ts`. - [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`. - [ ] Split provider discovery helpers out of `models-config.providers.ts`.

View File

@@ -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<OpenClawConfig["models"]>;
export type ModelsJsonPlan =
| {
action: "skip";
}
| {
action: "noop";
}
| {
action: "write";
contents: string;
};
async function resolveProvidersForModelsJson(params: {
cfg: OpenClawConfig;
agentDir: string;
env: NodeJS.ProcessEnv;
}): Promise<Record<string, ProviderConfig>> {
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<string> {
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<ModelsConfig["mode"]>;
existingParsed: unknown;
providers: Record<string, ProviderConfig>;
secretRefManagedProviders: ReadonlySet<string>;
explicitBaseUrlProviders: ReadonlySet<string>;
}): Promise<Record<string, ProviderConfig>> {
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<ModelsConfig["providers"]>[string]
>;
return mergeWithExistingProviderSecrets({
nextProviders: params.providers,
existingProviders: existingProviders as Record<string, ExistingProviderConfig>,
secretRefManagedProviders: params.secretRefManagedProviders,
explicitBaseUrlProviders: params.explicitBaseUrlProviders,
});
}
export async function planOpenClawModelsJson(params: {
cfg: OpenClawConfig;
agentDir: string;
env: NodeJS.ProcessEnv;
existingRaw: string;
existingParsed: unknown;
}): Promise<ModelsJsonPlan> {
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<string>();
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,
};
}

View File

@@ -7,22 +7,9 @@ import {
loadConfig, loadConfig,
} from "../config/config.js"; } from "../config/config.js";
import { createConfigRuntimeEnv } from "../config/env-vars.js"; import { createConfigRuntimeEnv } from "../config/env-vars.js";
import { isRecord } from "../utils.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js";
import { import { planOpenClawModelsJson } from "./models-config.plan.js";
mergeProviders,
mergeWithExistingProviderSecrets,
type ExistingProviderConfig,
} from "./models-config.merge.js";
import {
normalizeProviders,
type ProviderConfig,
resolveImplicitProviders,
} from "./models-config.providers.js";
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
const MODELS_JSON_WRITE_LOCKS = new Map<string, Promise<void>>(); const MODELS_JSON_WRITE_LOCKS = new Map<string, Promise<void>>();
async function readExistingModelsFile(pathname: string): Promise<{ 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<Record<string, ProviderConfig>> {
const { cfg, agentDir, env } = params;
const explicitProviders = cfg.models?.providers ?? {};
const implicitProviders = await resolveImplicitProviders({
agentDir,
config: cfg,
env,
explicitProviders,
});
const providers: Record<string, ProviderConfig> = mergeProviders({
implicit: implicitProviders,
explicit: explicitProviders,
});
return providers;
}
async function resolveProvidersForMode(params: {
mode: NonNullable<ModelsConfig["mode"]>;
existingParsed: unknown;
providers: Record<string, ProviderConfig>;
secretRefManagedProviders: ReadonlySet<string>;
explicitBaseUrlProviders: ReadonlySet<string>;
}): Promise<Record<string, ProviderConfig>> {
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<ModelsConfig["providers"]>[string]
>;
return mergeWithExistingProviderSecrets({
nextProviders: params.providers,
existingProviders: existingProviders as Record<string, ExistingProviderConfig>,
secretRefManagedProviders: params.secretRefManagedProviders,
explicitBaseUrlProviders: params.explicitBaseUrlProviders,
});
}
async function ensureModelsFileMode(pathname: string): Promise<void> { async function ensureModelsFileMode(pathname: string): Promise<void> {
await fs.chmod(pathname, 0o600).catch(() => { await fs.chmod(pathname, 0o600).catch(() => {
// best-effort // best-effort
@@ -147,50 +88,26 @@ export async function ensureOpenClawModelsJson(
// Ensure config env vars (e.g. AWS_PROFILE, AWS_ACCESS_KEY_ID) are // Ensure config env vars (e.g. AWS_PROFILE, AWS_ACCESS_KEY_ID) are
// are available to provider discovery without mutating process.env. // are available to provider discovery without mutating process.env.
const env = createConfigRuntimeEnv(cfg); 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 (plan.action === "skip") {
if (Object.keys(providers).length === 0) {
return { agentDir, wrote: false }; return { agentDir, wrote: false };
} }
const mode = cfg.models?.mode ?? DEFAULT_MODE; if (plan.action === "noop") {
const secretRefManagedProviders = new Set<string>();
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) {
await ensureModelsFileMode(targetPath); await ensureModelsFileMode(targetPath);
return { agentDir, wrote: false }; return { agentDir, wrote: false };
} }
await fs.mkdir(agentDir, { recursive: true, mode: 0o700 }); await fs.mkdir(agentDir, { recursive: true, mode: 0o700 });
await writeModelsFileAtomic(targetPath, next); await writeModelsFileAtomic(targetPath, plan.contents);
await ensureModelsFileMode(targetPath); await ensureModelsFileMode(targetPath);
return { agentDir, wrote: true }; return { agentDir, wrote: true };
}); });