2025-12-23 02:48:48 +01:00
|
|
|
import fs from "node:fs/promises";
|
|
|
|
|
import path from "node:path";
|
2026-01-30 03:15:10 +01:00
|
|
|
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
2026-02-09 17:02:55 -08:00
|
|
|
import { isRecord } from "../utils.js";
|
2026-01-30 03:15:10 +01:00
|
|
|
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
2026-01-13 04:04:11 +00:00
|
|
|
import {
|
|
|
|
|
normalizeProviders,
|
|
|
|
|
type ProviderConfig,
|
2026-01-23 16:50:30 -05:00
|
|
|
resolveImplicitBedrockProvider,
|
2026-01-13 05:24:41 +00:00
|
|
|
resolveImplicitCopilotProvider,
|
2026-01-13 04:04:11 +00:00
|
|
|
resolveImplicitProviders,
|
|
|
|
|
} from "./models-config.providers.js";
|
2025-12-23 02:48:48 +01:00
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
2025-12-23 02:48:48 +01:00
|
|
|
|
|
|
|
|
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
|
2026-01-12 16:52:32 +00:00
|
|
|
|
2026-01-14 14:31:43 +00:00
|
|
|
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
|
2026-01-13 04:04:11 +00:00
|
|
|
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
|
|
|
|
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
2026-01-31 16:19:20 +09:00
|
|
|
if (implicitModels.length === 0) {
|
|
|
|
|
return { ...implicit, ...explicit };
|
|
|
|
|
}
|
2026-01-13 04:04:11 +00:00
|
|
|
|
|
|
|
|
const getId = (model: unknown): string => {
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!model || typeof model !== "object") {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
2026-01-13 04:04:11 +00:00
|
|
|
const id = (model as { id?: unknown }).id;
|
|
|
|
|
return typeof id === "string" ? id.trim() : "";
|
|
|
|
|
};
|
2026-02-23 05:20:48 +01:00
|
|
|
const implicitById = new Map(
|
|
|
|
|
implicitModels.map((model) => [getId(model), model] as const).filter(([id]) => Boolean(id)),
|
|
|
|
|
);
|
|
|
|
|
const seen = new Set<string>();
|
2026-01-13 04:04:11 +00:00
|
|
|
|
2026-02-23 05:20:48 +01:00
|
|
|
const mergedModels = explicitModels.map((explicitModel) => {
|
|
|
|
|
const id = getId(explicitModel);
|
|
|
|
|
if (!id) {
|
|
|
|
|
return explicitModel;
|
|
|
|
|
}
|
|
|
|
|
seen.add(id);
|
|
|
|
|
const implicitModel = implicitById.get(id);
|
|
|
|
|
if (!implicitModel) {
|
|
|
|
|
return explicitModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh capability metadata from the implicit catalog while preserving
|
|
|
|
|
// user-specific fields (cost, headers, compat, etc.) on explicit entries.
|
2026-02-24 18:57:37 +09:00
|
|
|
// reasoning is treated as user-overridable: if the user has explicitly set
|
|
|
|
|
// it in their config (key present), honour that value; otherwise fall back
|
|
|
|
|
// to the built-in catalog default so new reasoning models work out of the
|
|
|
|
|
// box without requiring every user to configure it.
|
2026-02-23 05:20:48 +01:00
|
|
|
return {
|
|
|
|
|
...explicitModel,
|
|
|
|
|
input: implicitModel.input,
|
2026-02-24 18:57:37 +09:00
|
|
|
reasoning: "reasoning" in explicitModel ? explicitModel.reasoning : implicitModel.reasoning,
|
2026-02-23 05:20:48 +01:00
|
|
|
contextWindow: implicitModel.contextWindow,
|
|
|
|
|
maxTokens: implicitModel.maxTokens,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const implicitModel of implicitModels) {
|
|
|
|
|
const id = getId(implicitModel);
|
|
|
|
|
if (!id || seen.has(id)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seen.add(id);
|
|
|
|
|
mergedModels.push(implicitModel);
|
|
|
|
|
}
|
2026-01-12 19:08:53 +00:00
|
|
|
|
2026-01-13 04:04:11 +00:00
|
|
|
return {
|
|
|
|
|
...implicit,
|
|
|
|
|
...explicit,
|
|
|
|
|
models: mergedModels,
|
|
|
|
|
};
|
2026-01-12 19:08:53 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 04:04:11 +00:00
|
|
|
function mergeProviders(params: {
|
|
|
|
|
implicit?: Record<string, ProviderConfig> | null;
|
|
|
|
|
explicit?: Record<string, ProviderConfig> | null;
|
|
|
|
|
}): Record<string, ProviderConfig> {
|
2026-01-14 14:31:43 +00:00
|
|
|
const out: Record<string, ProviderConfig> = params.implicit ? { ...params.implicit } : {};
|
2026-01-13 04:04:11 +00:00
|
|
|
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
|
|
|
|
|
const providerKey = key.trim();
|
2026-01-31 16:19:20 +09:00
|
|
|
if (!providerKey) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-13 04:04:11 +00:00
|
|
|
const implicit = out[providerKey];
|
2026-01-14 14:31:43 +00:00
|
|
|
out[providerKey] = implicit ? mergeProviderModels(implicit, explicit) : explicit;
|
2026-01-12 19:08:53 +00:00
|
|
|
}
|
2026-01-13 04:04:11 +00:00
|
|
|
return out;
|
2026-01-12 19:08:53 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-23 03:00:04 +01:00
|
|
|
async function readJson(pathname: string): Promise<unknown> {
|
2025-12-23 02:48:48 +01:00
|
|
|
try {
|
|
|
|
|
const raw = await fs.readFile(pathname, "utf8");
|
|
|
|
|
return JSON.parse(raw) as unknown;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export async function ensureOpenClawModelsJson(
|
|
|
|
|
config?: OpenClawConfig,
|
2026-01-06 18:25:37 +00:00
|
|
|
agentDirOverride?: string,
|
2025-12-23 02:48:48 +01:00
|
|
|
): Promise<{ agentDir: string; wrote: boolean }> {
|
|
|
|
|
const cfg = config ?? loadConfig();
|
2026-01-30 03:15:10 +01:00
|
|
|
const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir();
|
2026-01-13 04:04:11 +00:00
|
|
|
|
2026-01-31 16:03:28 +09:00
|
|
|
const explicitProviders = cfg.models?.providers ?? {};
|
2026-02-11 15:51:59 +00:00
|
|
|
const implicitProviders = await resolveImplicitProviders({ agentDir, explicitProviders });
|
2026-01-13 04:04:11 +00:00
|
|
|
const providers: Record<string, ProviderConfig> = mergeProviders({
|
|
|
|
|
implicit: implicitProviders,
|
|
|
|
|
explicit: explicitProviders,
|
|
|
|
|
});
|
2026-01-23 16:50:30 -05:00
|
|
|
const implicitBedrock = await resolveImplicitBedrockProvider({ agentDir, config: cfg });
|
|
|
|
|
if (implicitBedrock) {
|
|
|
|
|
const existing = providers["amazon-bedrock"];
|
|
|
|
|
providers["amazon-bedrock"] = existing
|
|
|
|
|
? mergeProviderModels(implicitBedrock, existing)
|
|
|
|
|
: implicitBedrock;
|
|
|
|
|
}
|
2026-01-13 05:24:41 +00:00
|
|
|
const implicitCopilot = await resolveImplicitCopilotProvider({ agentDir });
|
2026-01-13 04:04:11 +00:00
|
|
|
if (implicitCopilot && !providers["github-copilot"]) {
|
|
|
|
|
providers["github-copilot"] = implicitCopilot;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 11:04:58 +00:00
|
|
|
if (Object.keys(providers).length === 0) {
|
2026-01-06 18:25:37 +00:00
|
|
|
return { agentDir, wrote: false };
|
2025-12-23 02:48:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mode = cfg.models?.mode ?? DEFAULT_MODE;
|
|
|
|
|
const targetPath = path.join(agentDir, "models.json");
|
|
|
|
|
|
|
|
|
|
let mergedProviders = providers;
|
|
|
|
|
let existingRaw = "";
|
|
|
|
|
if (mode === "merge") {
|
|
|
|
|
const existing = await readJson(targetPath);
|
|
|
|
|
if (isRecord(existing) && isRecord(existing.providers)) {
|
|
|
|
|
const existingProviders = existing.providers as Record<
|
|
|
|
|
string,
|
|
|
|
|
NonNullable<ModelsConfig["providers"]>[string]
|
|
|
|
|
>;
|
|
|
|
|
mergedProviders = { ...existingProviders, ...providers };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 04:04:11 +00:00
|
|
|
const normalizedProviders = normalizeProviders({
|
|
|
|
|
providers: mergedProviders,
|
|
|
|
|
agentDir,
|
|
|
|
|
});
|
2026-01-12 06:58:31 +00:00
|
|
|
const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`;
|
2025-12-23 02:48:48 +01:00
|
|
|
try {
|
|
|
|
|
existingRaw = await fs.readFile(targetPath, "utf8");
|
|
|
|
|
} catch {
|
|
|
|
|
existingRaw = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existingRaw === next) {
|
|
|
|
|
return { agentDir, wrote: false };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.mkdir(agentDir, { recursive: true, mode: 0o700 });
|
|
|
|
|
await fs.writeFile(targetPath, next, { mode: 0o600 });
|
|
|
|
|
return { agentDir, wrote: true };
|
|
|
|
|
}
|