diff --git a/src/agents/models-config.test.ts b/src/agents/models-config.test.ts index 0ad66e87b..481f4a0a9 100644 --- a/src/agents/models-config.test.ts +++ b/src/agents/models-config.test.ts @@ -487,9 +487,14 @@ describe("models config", () => { const modelPath = path.join(resolveClawdbotAgentDir(), "models.json"); const raw = await fs.readFile(modelPath, "utf8"); const parsed = JSON.parse(raw) as { - providers: Record; + providers: Record< + string, + { apiKey?: string; models?: Array<{ id: string }> } + >; }; expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY"); + const ids = parsed.providers.minimax?.models?.map((model) => model.id); + expect(ids).toContain("MiniMax-VL-01"); } finally { if (prevKey === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = prevKey; diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 5476b517d..28fc73723 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -25,6 +25,57 @@ function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } +function mergeProviderModels( + implicit: ProviderConfig, + explicit: ProviderConfig, +): ProviderConfig { + const implicitModels = Array.isArray(implicit.models) ? implicit.models : []; + const explicitModels = Array.isArray(explicit.models) ? explicit.models : []; + if (implicitModels.length === 0) return { ...implicit, ...explicit }; + + const getId = (model: unknown): string => { + if (!model || typeof model !== "object") return ""; + const id = (model as { id?: unknown }).id; + return typeof id === "string" ? id.trim() : ""; + }; + const seen = new Set(explicitModels.map(getId).filter(Boolean)); + + const mergedModels = [ + ...explicitModels, + ...implicitModels.filter((model) => { + const id = getId(model); + if (!id) return false; + if (seen.has(id)) return false; + seen.add(id); + return true; + }), + ]; + + return { + ...implicit, + ...explicit, + models: mergedModels, + }; +} + +function mergeProviders(params: { + implicit?: Record | null; + explicit?: Record | null; +}): Record { + const out: Record = params.implicit + ? { ...params.implicit } + : {}; + for (const [key, explicit] of Object.entries(params.explicit ?? {})) { + const providerKey = key.trim(); + if (!providerKey) continue; + const implicit = out[providerKey]; + out[providerKey] = implicit + ? mergeProviderModels(implicit, explicit) + : explicit; + } + return out; +} + async function readJson(pathname: string): Promise { try { const raw = await fs.readFile(pathname, "utf8"); @@ -101,12 +152,15 @@ export async function ensureClawdbotModelsJson( ? agentDirOverride.trim() : resolveClawdbotAgentDir(); - const explicitProviders = cfg.models?.providers ?? {}; + const explicitProviders = (cfg.models?.providers ?? {}) as Record< + string, + ProviderConfig + >; const implicitProviders = resolveImplicitProviders({ agentDir }); - const providers: Record = { - ...implicitProviders, - ...explicitProviders, - }; + const providers: Record = mergeProviders({ + implicit: implicitProviders, + explicit: explicitProviders, + }); const implicitCopilot = await maybeBuildCopilotProvider({ agentDir }); if (implicitCopilot && !providers["github-copilot"]) { providers["github-copilot"] = implicitCopilot;