mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 23:20:21 +00:00
Plugins: avoid booting bundled providers for catalog hooks
This commit is contained in:
97
src/plugins/provider-catalog-metadata.ts
Normal file
97
src/plugins/provider-catalog-metadata.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type {
|
||||
ProviderAugmentModelCatalogContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
} from "./types.js";
|
||||
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
||||
const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]);
|
||||
|
||||
function findCatalogTemplate(params: {
|
||||
entries: ReadonlyArray<{ provider: string; id: string }>;
|
||||
providerId: string;
|
||||
templateIds: readonly string[];
|
||||
}) {
|
||||
return params.templateIds
|
||||
.map((templateId) =>
|
||||
params.entries.find(
|
||||
(entry) =>
|
||||
entry.provider.toLowerCase() === params.providerId.toLowerCase() &&
|
||||
entry.id.toLowerCase() === templateId.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.find((entry) => entry !== undefined);
|
||||
}
|
||||
|
||||
export function resolveBundledProviderBuiltInModelSuppression(
|
||||
context: ProviderBuiltInModelSuppressionContext,
|
||||
) {
|
||||
if (
|
||||
!SUPPRESSED_SPARK_PROVIDERS.has(normalizeProviderId(context.provider)) ||
|
||||
context.modelId.toLowerCase() !== OPENAI_DIRECT_SPARK_MODEL_ID
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
suppress: true,
|
||||
errorMessage: `Unknown model: ${context.provider}/${OPENAI_DIRECT_SPARK_MODEL_ID}. ${OPENAI_DIRECT_SPARK_MODEL_ID} is only supported via openai-codex OAuth. Use openai-codex/${OPENAI_DIRECT_SPARK_MODEL_ID}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function augmentBundledProviderCatalog(
|
||||
context: ProviderAugmentModelCatalogContext,
|
||||
): ProviderAugmentModelCatalogContext["entries"] {
|
||||
const openAiGpt54Template = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.2"],
|
||||
});
|
||||
const openAiGpt54ProTemplate = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.2-pro", "gpt-5.2"],
|
||||
});
|
||||
const openAiCodexGpt54Template = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.3-codex", "gpt-5.2-codex"],
|
||||
});
|
||||
const openAiCodexSparkTemplate = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.3-codex", "gpt-5.2-codex"],
|
||||
});
|
||||
|
||||
return [
|
||||
openAiGpt54Template
|
||||
? {
|
||||
...openAiGpt54Template,
|
||||
id: "gpt-5.4",
|
||||
name: "gpt-5.4",
|
||||
}
|
||||
: undefined,
|
||||
openAiGpt54ProTemplate
|
||||
? {
|
||||
...openAiGpt54ProTemplate,
|
||||
id: "gpt-5.4-pro",
|
||||
name: "gpt-5.4-pro",
|
||||
}
|
||||
: undefined,
|
||||
openAiCodexGpt54Template
|
||||
? {
|
||||
...openAiCodexGpt54Template,
|
||||
id: "gpt-5.4",
|
||||
name: "gpt-5.4",
|
||||
}
|
||||
: undefined,
|
||||
openAiCodexSparkTemplate
|
||||
? {
|
||||
...openAiCodexSparkTemplate,
|
||||
id: OPENAI_DIRECT_SPARK_MODEL_ID,
|
||||
name: OPENAI_DIRECT_SPARK_MODEL_ID,
|
||||
}
|
||||
: undefined,
|
||||
].filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
|
||||
}
|
||||
@@ -2,16 +2,23 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin, ProviderRuntimeModel } from "./types.js";
|
||||
|
||||
type ResolvePluginProviders = typeof import("./providers.js").resolvePluginProviders;
|
||||
type ResolveNonBundledProviderPluginIds =
|
||||
typeof import("./providers.js").resolveNonBundledProviderPluginIds;
|
||||
type ResolveOwningPluginIdsForProvider =
|
||||
typeof import("./providers.js").resolveOwningPluginIdsForProvider;
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn<ResolvePluginProviders>((_) => [] as ProviderPlugin[]);
|
||||
const resolveNonBundledProviderPluginIdsMock = vi.fn<ResolveNonBundledProviderPluginIds>(
|
||||
(_) => [] as string[],
|
||||
);
|
||||
const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForProvider>(
|
||||
(_) => undefined as string[] | undefined,
|
||||
);
|
||||
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
|
||||
resolveNonBundledProviderPluginIds: (params: unknown) =>
|
||||
resolveNonBundledProviderPluginIdsMock(params as never),
|
||||
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
||||
resolveOwningPluginIdsForProviderMock(params as never),
|
||||
}));
|
||||
@@ -34,6 +41,7 @@ import {
|
||||
normalizeProviderResolvedModelWithPlugin,
|
||||
prepareProviderDynamicModel,
|
||||
prepareProviderRuntimeAuth,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
refreshProviderOAuthCredentialWithPlugin,
|
||||
resolveProviderRuntimePlugin,
|
||||
runProviderDynamicModel,
|
||||
@@ -55,8 +63,11 @@ const MODEL: ProviderRuntimeModel = {
|
||||
|
||||
describe("provider-runtime", () => {
|
||||
beforeEach(() => {
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
resolveNonBundledProviderPluginIdsMock.mockReset();
|
||||
resolveNonBundledProviderPluginIdsMock.mockReturnValue([]);
|
||||
resolveOwningPluginIdsForProviderMock.mockReset();
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
|
||||
});
|
||||
@@ -454,4 +465,44 @@ describe("provider-runtime", () => {
|
||||
expect(resolveUsageAuth).toHaveBeenCalledTimes(1);
|
||||
expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves bundled catalog hooks without loading provider plugins", async () => {
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
suppress: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
augmentModelCatalogWithProviderPlugins({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
entries: [
|
||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ provider: "openai", id: "gpt-5.2-pro", name: "GPT-5.2 Pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "gpt-5.3-codex-spark",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
|
||||
import {
|
||||
augmentBundledProviderCatalog,
|
||||
resolveBundledProviderBuiltInModelSuppression,
|
||||
} from "./provider-catalog-metadata.js";
|
||||
import {
|
||||
resolveNonBundledProviderPluginIds,
|
||||
resolveOwningPluginIdsForProvider,
|
||||
resolvePluginProviders,
|
||||
} from "./providers.js";
|
||||
import type {
|
||||
ProviderAuthDoctorHintContext,
|
||||
ProviderAugmentModelCatalogContext,
|
||||
@@ -33,19 +41,104 @@ function matchesProviderId(provider: ProviderPlugin, providerId: string): boolea
|
||||
return (provider.aliases ?? []).some((alias) => normalizeProviderId(alias) === normalized);
|
||||
}
|
||||
|
||||
let cachedHookProvidersWithoutConfig = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, ProviderPlugin[]>
|
||||
>();
|
||||
let cachedHookProvidersByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
|
||||
>();
|
||||
|
||||
function resolveHookProviderCacheBucket(params: {
|
||||
config?: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
if (!params.config) {
|
||||
let bucket = cachedHookProvidersWithoutConfig.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, ProviderPlugin[]>();
|
||||
cachedHookProvidersWithoutConfig.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
let envBuckets = cachedHookProvidersByConfig.get(params.config);
|
||||
if (!envBuckets) {
|
||||
envBuckets = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
|
||||
cachedHookProvidersByConfig.set(params.config, envBuckets);
|
||||
}
|
||||
let bucket = envBuckets.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, ProviderPlugin[]>();
|
||||
envBuckets.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function buildHookProviderCacheKey(params: { workspaceDir?: string; onlyPluginIds?: string[] }) {
|
||||
return `${params.workspaceDir ?? ""}::${JSON.stringify(params.onlyPluginIds ?? [])}`;
|
||||
}
|
||||
|
||||
export function resetProviderRuntimeHookCacheForTest(): void {
|
||||
cachedHookProvidersWithoutConfig = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, ProviderPlugin[]>
|
||||
>();
|
||||
cachedHookProvidersByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
|
||||
>();
|
||||
}
|
||||
|
||||
function resolveProviderPluginsForHooks(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): ProviderPlugin[] {
|
||||
return resolvePluginProviders({
|
||||
const env = params.env ?? process.env;
|
||||
const cacheBucket = resolveHookProviderCacheBucket({
|
||||
config: params.config,
|
||||
env,
|
||||
});
|
||||
const cacheKey = buildHookProviderCacheKey({
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
const cached = cacheBucket.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = resolvePluginProviders({
|
||||
...params,
|
||||
env,
|
||||
activate: false,
|
||||
cache: false,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
cacheBucket.set(cacheKey, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveProviderPluginsForCatalogHooks(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
const onlyPluginIds = resolveNonBundledProviderPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
if (onlyPluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
onlyPluginIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderRuntimePlugin(params: {
|
||||
@@ -265,7 +358,11 @@ export function resolveProviderBuiltInModelSuppression(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderBuiltInModelSuppressionContext;
|
||||
}) {
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const bundledResult = resolveBundledProviderBuiltInModelSuppression(params.context);
|
||||
if (bundledResult?.suppress) {
|
||||
return bundledResult;
|
||||
}
|
||||
for (const plugin of resolveProviderPluginsForCatalogHooks(params)) {
|
||||
const result = plugin.suppressBuiltInModel?.(params.context);
|
||||
if (result?.suppress) {
|
||||
return result;
|
||||
@@ -280,8 +377,10 @@ export async function augmentModelCatalogWithProviderPlugins(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderAugmentModelCatalogContext;
|
||||
}) {
|
||||
const supplemental = [] as ProviderAugmentModelCatalogContext["entries"];
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const supplemental = [
|
||||
...augmentBundledProviderCatalog(params.context),
|
||||
] as ProviderAugmentModelCatalogContext["entries"];
|
||||
for (const plugin of resolveProviderPluginsForCatalogHooks(params)) {
|
||||
const next = await plugin.augmentModelCatalog?.(params.context);
|
||||
if (!next || next.length === 0) {
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withBundledPluginAllowlistCompat } from "./bundled-compat.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
@@ -106,6 +107,33 @@ export function resolveOwningPluginIdsForProvider(params: {
|
||||
return pluginIds.length > 0 ? pluginIds : undefined;
|
||||
}
|
||||
|
||||
export function resolveNonBundledProviderPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
|
||||
return registry.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin !== "bundled" &&
|
||||
plugin.providers.length > 0 &&
|
||||
resolveEffectiveEnableState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedConfig,
|
||||
rootConfig: params.config,
|
||||
}).enabled,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
|
||||
Reference in New Issue
Block a user