mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
fix: bound unscoped provider discovery fallback
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"id": "arcee",
|
||||
"enabledByDefault": true,
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providers": ["arcee"],
|
||||
"providerAuthEnvVars": {
|
||||
"arcee": ["ARCEEAI_API_KEY"]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { buildArceeProvider } from "./provider-catalog.js";
|
||||
|
||||
export const arceeProviderDiscovery: ProviderPlugin = {
|
||||
id: "arcee",
|
||||
label: "Arcee AI",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
staticCatalog: {
|
||||
order: "simple",
|
||||
run: async () => ({
|
||||
provider: buildArceeProvider(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default arceeProviderDiscovery;
|
||||
@@ -44,6 +44,19 @@ function createManifestPlugin(id: string): PluginManifestRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function createManifestPluginWithoutDiscovery(params: {
|
||||
id: string;
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
}): PluginManifestRecord {
|
||||
const { providerDiscoverySource: _providerDiscoverySource, ...plugin } = createManifestPlugin(
|
||||
params.id,
|
||||
);
|
||||
return {
|
||||
...plugin,
|
||||
...(params.providerAuthEnvVars ? { providerAuthEnvVars: params.providerAuthEnvVars } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function createProvider(params: { id: string; mode: "static" | "catalog" }): ProviderPlugin {
|
||||
const hook = {
|
||||
run: async () => ({
|
||||
@@ -80,28 +93,55 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
||||
expect(mocks.resolvePluginProviders).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
bundledProviderAllowlistCompat: true,
|
||||
onlyPluginIds: ["deepseek"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to full provider plugins for mixed live and static-only entries", () => {
|
||||
it("keeps unscoped discovery bounded for mixed live and static-only entries", () => {
|
||||
const codexEntryProvider = createProvider({ id: "codex", mode: "catalog" });
|
||||
const fullProviders = [
|
||||
createProvider({ id: "codex", mode: "catalog" }),
|
||||
createProvider({ id: "deepseek", mode: "catalog" }),
|
||||
createProvider({ id: "kilocode", mode: "catalog" }),
|
||||
];
|
||||
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["codex", "deepseek"]);
|
||||
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue([
|
||||
"codex",
|
||||
"deepseek",
|
||||
"kilocode",
|
||||
"unused",
|
||||
]);
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [createManifestPlugin("codex"), createManifestPlugin("deepseek")],
|
||||
plugins: [
|
||||
createManifestPlugin("codex"),
|
||||
createManifestPlugin("deepseek"),
|
||||
createManifestPluginWithoutDiscovery({
|
||||
id: "kilocode",
|
||||
providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] },
|
||||
}),
|
||||
createManifestPluginWithoutDiscovery({
|
||||
id: "unused",
|
||||
providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] },
|
||||
}),
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.loadSource.mockImplementation((modulePath: string) =>
|
||||
modulePath.includes("/codex/")
|
||||
? createProvider({ id: "codex", mode: "catalog" })
|
||||
? codexEntryProvider
|
||||
: createProvider({ id: "deepseek", mode: "static" }),
|
||||
);
|
||||
mocks.resolvePluginProviders.mockReturnValue(fullProviders);
|
||||
|
||||
expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual(fullProviders);
|
||||
expect(
|
||||
resolvePluginDiscoveryProvidersRuntime({
|
||||
env: { KILOCODE_API_KEY: "sk-test" } as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toEqual([{ ...codexEntryProvider, pluginId: "codex" }, ...fullProviders]);
|
||||
expect(mocks.resolvePluginProviders).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["deepseek", "kilocode"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns static-only discovery entries for callers that explicitly request them", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
|
||||
import { resolvePluginProviders } from "./providers.runtime.js";
|
||||
import { createPluginSourceLoader } from "./source-loader.js";
|
||||
@@ -17,6 +17,8 @@ type ProviderDiscoveryModule =
|
||||
type ProviderDiscoveryEntryResult = {
|
||||
providers: ProviderPlugin[];
|
||||
complete: boolean;
|
||||
pluginRecords: PluginManifestRecord[];
|
||||
entryPluginIds: Set<string>;
|
||||
};
|
||||
|
||||
function normalizeDiscoveryModule(value: ProviderDiscoveryModule): ProviderPlugin[] {
|
||||
@@ -48,6 +50,22 @@ function hasLiveProviderDiscoveryHook(provider: ProviderPlugin): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasProviderAuthEnvCredential(
|
||||
plugin: PluginManifestRecord,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
return Object.values(plugin.providerAuthEnvVars ?? {}).some((envVars) =>
|
||||
envVars.some((name) => {
|
||||
const value = env[name]?.trim();
|
||||
return value !== undefined && value !== "";
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeSorted(values: Iterable<string>): string[] {
|
||||
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveProviderDiscoveryEntryPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -59,19 +77,21 @@ function resolveProviderDiscoveryEntryPlugins(params: {
|
||||
}): ProviderDiscoveryEntryResult {
|
||||
const pluginIds = resolveDiscoveredProviderPluginIds(params);
|
||||
const pluginIdSet = new Set(pluginIds);
|
||||
const records = loadPluginManifestRegistry(params).plugins.filter(
|
||||
(plugin) => plugin.providerDiscoverySource && pluginIdSet.has(plugin.id),
|
||||
const pluginRecords = loadPluginManifestRegistry(params).plugins.filter((plugin) =>
|
||||
pluginIdSet.has(plugin.id),
|
||||
);
|
||||
if (records.length === 0) {
|
||||
return { providers: [], complete: false };
|
||||
const entryRecords = pluginRecords.filter((plugin) => plugin.providerDiscoverySource);
|
||||
const entryPluginIds = new Set(entryRecords.map((plugin) => plugin.id));
|
||||
if (entryRecords.length === 0) {
|
||||
return { providers: [], complete: false, pluginRecords, entryPluginIds };
|
||||
}
|
||||
const complete = records.length === pluginIdSet.size;
|
||||
const complete = entryRecords.length === pluginIdSet.size;
|
||||
if (params.requireCompleteDiscoveryEntryCoverage && !complete) {
|
||||
return { providers: [], complete: false };
|
||||
return { providers: [], complete: false, pluginRecords, entryPluginIds };
|
||||
}
|
||||
const loadSource = createPluginSourceLoader();
|
||||
const providers: ProviderPlugin[] = [];
|
||||
for (const manifest of records) {
|
||||
for (const manifest of entryRecords) {
|
||||
try {
|
||||
const moduleExport = loadSource(manifest.providerDiscoverySource!) as ProviderDiscoveryModule;
|
||||
providers.push(
|
||||
@@ -82,10 +102,26 @@ function resolveProviderDiscoveryEntryPlugins(params: {
|
||||
} catch {
|
||||
// Discovery fast path is optional. Fall back to the full plugin loader
|
||||
// below so existing plugin diagnostics/load behavior remains canonical.
|
||||
return { providers: [], complete: false };
|
||||
return { providers: [], complete: false, pluginRecords, entryPluginIds };
|
||||
}
|
||||
}
|
||||
return { providers, complete };
|
||||
return { providers, complete, pluginRecords, entryPluginIds };
|
||||
}
|
||||
|
||||
function resolveSelectiveFullPluginIds(params: {
|
||||
entryResult: ProviderDiscoveryEntryResult;
|
||||
entryProviders: ProviderPlugin[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] {
|
||||
const staticOnlyEntryPluginIds = params.entryProviders
|
||||
.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 dedupeSorted([...staticOnlyEntryPluginIds, ...missingEntryCredentialPluginIds]);
|
||||
}
|
||||
|
||||
export function resolvePluginDiscoveryProvidersRuntime(params: {
|
||||
@@ -97,19 +133,35 @@ export function resolvePluginDiscoveryProvidersRuntime(params: {
|
||||
requireCompleteDiscoveryEntryCoverage?: boolean;
|
||||
discoveryEntriesOnly?: boolean;
|
||||
}): ProviderPlugin[] {
|
||||
const env = params.env ?? process.env;
|
||||
const entryResult = resolveProviderDiscoveryEntryPlugins(params);
|
||||
if (params.discoveryEntriesOnly === true) {
|
||||
return entryResult.providers;
|
||||
}
|
||||
if (
|
||||
entryResult.complete &&
|
||||
entryResult.providers.length > 0 &&
|
||||
entryResult.providers.every(hasLiveProviderDiscoveryHook)
|
||||
) {
|
||||
return entryResult.providers;
|
||||
const liveEntryProviders = entryResult.providers.filter(hasLiveProviderDiscoveryHook);
|
||||
if (entryResult.complete && liveEntryProviders.length === entryResult.providers.length) {
|
||||
return liveEntryProviders;
|
||||
}
|
||||
if (params.onlyPluginIds === undefined && entryResult.providers.length > 0) {
|
||||
const fullPluginIds = resolveSelectiveFullPluginIds({
|
||||
entryResult,
|
||||
entryProviders: entryResult.providers,
|
||||
env,
|
||||
});
|
||||
const fullProviders =
|
||||
fullPluginIds.length > 0
|
||||
? resolvePluginProviders({
|
||||
...params,
|
||||
env,
|
||||
onlyPluginIds: fullPluginIds,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
})
|
||||
: [];
|
||||
return [...liveEntryProviders, ...fullProviders];
|
||||
}
|
||||
return resolvePluginProviders({
|
||||
...params,
|
||||
env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user