fix: keep provider catalog entries on release live path

This commit is contained in:
Peter Steinberger
2026-05-25 07:26:01 +01:00
parent 1a46c08dd6
commit 709944b0b5
3 changed files with 87 additions and 33 deletions

View File

@@ -1943,6 +1943,65 @@ function createStaticLiveModelRegistry(models: Array<Model>): LiveModelRegistry
};
}
function toLiveModelConfig(model: Model): NonNullable<ModelProviderConfig["models"]>[number] {
return {
id: model.id,
name: model.name,
api: model.api as ModelProviderConfig["api"],
baseUrl: model.baseUrl,
input: model.input ?? ["text"],
reasoning: model.reasoning,
cost: model.cost,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
...(model.compat ? { compat: model.compat } : {}),
};
}
function mergeLiveProviderConfig(params: {
base: ModelProviderConfig | undefined;
discovered: ModelProviderConfig;
}): ModelProviderConfig {
const baseModels = params.base?.models ?? [];
const discoveredModels = params.discovered.models ?? [];
const mergedModels = new Map<string, NonNullable<ModelProviderConfig["models"]>[number]>();
for (const model of discoveredModels) {
if (model.id) {
mergedModels.set(model.id, model);
}
}
for (const model of baseModels) {
if (model.id) {
mergedModels.set(model.id, model);
}
}
return {
...params.discovered,
...params.base,
api: params.base?.api ?? params.discovered.api,
baseUrl: params.base?.baseUrl ?? params.discovered.baseUrl,
models: [...mergedModels.values()],
};
}
function buildLiveProviderConfigs(candidates: Array<Model>): Record<string, ModelProviderConfig> {
const providers: Record<string, ModelProviderConfig> = {};
for (const model of candidates) {
const existing = providers[model.provider];
if (existing) {
existing.models ??= [];
existing.models.push(toLiveModelConfig(model));
continue;
}
providers[model.provider] = {
api: model.api as ModelProviderConfig["api"],
baseUrl: model.baseUrl,
models: [toLiveModelConfig(model)],
};
}
return providers;
}
function parseExplicitLiveModelRef(
raw: string,
providerFilter: Set<string> | null,
@@ -2045,8 +2104,16 @@ function buildLiveGatewayConfig(params: {
const providerOverrides = params.providerOverrides ?? {};
const lmstudioProvider = params.cfg.models?.providers?.lmstudio;
const baseProviders = params.cfg.models?.providers ?? {};
const candidateProviders = buildLiveProviderConfigs(params.candidates);
const discoveredProviders = Object.fromEntries(
Object.entries(candidateProviders).map(([provider, discovered]) => [
provider,
mergeLiveProviderConfig({ base: baseProviders[provider], discovered }),
]),
);
const nextProviders = {
...baseProviders,
...discoveredProviders,
...(lmstudioProvider
? {
lmstudio: {
@@ -2954,15 +3021,15 @@ describeLive("gateway live (dev agent, profile keys)", () => {
);
const workspaceDir = resolveAgentWorkspaceDir(cfg, DEFAULT_AGENT_ID);
logProgress("[all-models] preparing models.json");
await withGatewayLiveSetupTimeout(
const modelsJsonResult = await withGatewayLiveSetupTimeout(
ensureOpenClawModelsJson(cfg, undefined, {
workspaceDir,
...(providerList ? { providerDiscoveryProviderIds: providerList } : {}),
}),
"[all-models] prepare models.json",
);
const agentDir = modelsJsonResult.agentDir;
const agentDir = resolveDefaultAgentDir(cfg);
const rawModels = process.env.OPENCLAW_LIVE_GATEWAY_MODELS?.trim();
const useModern = !rawModels || rawModels === "modern" || rawModels === "all";
const useExplicit = Boolean(rawModels) && !useModern;

View File

@@ -157,23 +157,20 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
);
});
it("falls back to full provider plugins when discovery entries only expose static catalogs", () => {
const fullProvider = createProvider({ id: "deepseek", mode: "catalog" });
mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "static" }));
mocks.resolvePluginProviders.mockReturnValue([fullProvider]);
it("uses static provider catalog entries without loading the full plugin", () => {
const staticProvider = createProvider({ id: "deepseek", mode: "static" });
mocks.loadSource.mockReturnValue(staticProvider);
expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual([fullProvider]);
expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1);
const params = requireResolvePluginProvidersParams();
expect(params.onlyPluginIds).toEqual(["deepseek"]);
expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual([
{ ...staticProvider, pluginId: "deepseek" },
]);
expect(mocks.resolvePluginProviders).not.toHaveBeenCalled();
});
it("keeps unscoped discovery bounded for mixed live and static-only entries", () => {
const codexEntryProvider = createProvider({ id: "codex", mode: "catalog" });
const fullProviders = [
createProvider({ id: "deepseek", mode: "catalog" }),
createProvider({ id: "kilocode", mode: "catalog" }),
];
const deepseekEntryProvider = createProvider({ id: "deepseek", mode: "static" });
const fullProviders = [createProvider({ id: "kilocode", mode: "catalog" })];
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue([
"codex",
"deepseek",
@@ -199,9 +196,7 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
},
});
mocks.loadSource.mockImplementation((modulePath: string) =>
modulePath.includes("/codex/")
? codexEntryProvider
: createProvider({ id: "deepseek", mode: "static" }),
modulePath.includes("/codex/") ? codexEntryProvider : deepseekEntryProvider,
);
mocks.resolvePluginProviders.mockReturnValue(fullProviders);
@@ -209,10 +204,14 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
resolvePluginDiscoveryProvidersRuntime({
env: { KILOCODE_API_KEY: "sk-test" } as NodeJS.ProcessEnv,
}),
).toEqual([{ ...codexEntryProvider, pluginId: "codex" }, ...fullProviders]);
).toEqual([
{ ...codexEntryProvider, pluginId: "codex" },
{ ...deepseekEntryProvider, pluginId: "deepseek" },
...fullProviders,
]);
expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1);
const params = requireResolvePluginProvidersParams();
expect(params.onlyPluginIds).toEqual(["deepseek", "kilocode"]);
expect(params.onlyPluginIds).toEqual(["kilocode"]);
});
it("falls back to full provider plugins when setup provider env vars are configured", () => {

View File

@@ -262,24 +262,13 @@ function resolveProviderDiscoveryEntryPlugins(params: {
function resolveSelectiveFullPluginIds(params: {
entryResult: ProviderDiscoveryEntryResult;
runtimeEntryProviders: ProviderPlugin[];
env: NodeJS.ProcessEnv;
}): string[] {
const runtimeEntryProviderIds = new Set(
params.runtimeEntryProviders
.map((provider) => provider.pluginId)
.filter((pluginId): pluginId is string => typeof pluginId === "string" && pluginId !== ""),
);
const staticOnlyEntryPluginIds = params.entryResult.providers
.filter((provider) => !runtimeEntryProviderIds.has(provider.pluginId ?? ""))
.filter((provider) => !hasLiveProviderDiscoveryHook(provider))
.map((provider) => provider.pluginId)
.filter((pluginId): pluginId is string => typeof pluginId === "string" && pluginId !== "");
const missingEntryCredentialPluginIds = params.entryResult.pluginRecords
.filter((plugin) => !params.entryResult.entryPluginIds.has(plugin.id))
.filter((plugin) => hasProviderAuthEnvCredential(plugin, params.env))
.map((plugin) => plugin.id);
return sortUniqueStrings([...staticOnlyEntryPluginIds, ...missingEntryCredentialPluginIds]);
return sortUniqueStrings(missingEntryCredentialPluginIds);
}
function resolveMissingEntryPluginIds(entryResult: ProviderDiscoveryEntryResult): string[] {
@@ -295,7 +284,7 @@ function resolveRuntimeEntryProviders(entryResult: ProviderDiscoveryEntryResult)
}
return Boolean(
provider.pluginId &&
entryResult.manifestEntryPluginIds.has(provider.pluginId) &&
entryResult.entryPluginIds.has(provider.pluginId) &&
typeof provider.staticCatalog?.run === "function",
);
});
@@ -324,7 +313,6 @@ export function resolvePluginDiscoveryProvidersRuntime(params: {
if (params.onlyPluginIds === undefined && runtimeEntryProviders.length > 0) {
const fullPluginIds = resolveSelectiveFullPluginIds({
entryResult,
runtimeEntryProviders,
env,
});
const fullProviders =