mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
fix(agents): avoid provider startup scans
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"id": "anthropic",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["anthropic"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"modelSupport": {
|
||||
"modelPrefixes": ["claude-"]
|
||||
},
|
||||
|
||||
35
extensions/anthropic/provider-discovery.ts
Normal file
35
extensions/anthropic/provider-discovery.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { readClaudeCliCredentialsForRuntime } from "./cli-auth-seam.js";
|
||||
|
||||
const CLAUDE_CLI_BACKEND_ID = "claude-cli";
|
||||
|
||||
function resolveClaudeCliSyntheticAuth() {
|
||||
const credential = readClaudeCliCredentialsForRuntime();
|
||||
if (!credential) {
|
||||
return undefined;
|
||||
}
|
||||
return credential.type === "oauth"
|
||||
? {
|
||||
apiKey: credential.access,
|
||||
source: "Claude CLI native auth",
|
||||
mode: "oauth" as const,
|
||||
expiresAt: credential.expires,
|
||||
}
|
||||
: {
|
||||
apiKey: credential.token,
|
||||
source: "Claude CLI native auth",
|
||||
mode: "token" as const,
|
||||
expiresAt: credential.expires,
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicProviderDiscovery: ProviderPlugin = {
|
||||
id: CLAUDE_CLI_BACKEND_ID,
|
||||
label: "Claude CLI",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
resolveSyntheticAuth: ({ provider }) =>
|
||||
provider === CLAUDE_CLI_BACKEND_ID ? resolveClaudeCliSyntheticAuth() : undefined,
|
||||
};
|
||||
|
||||
export default anthropicProviderDiscovery;
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
OLLAMA_DEFAULT_API_KEY,
|
||||
OLLAMA_PROVIDER_ID,
|
||||
hasMeaningfulExplicitOllamaConfig,
|
||||
resolveOllamaDiscoveryResult,
|
||||
type OllamaPluginConfig,
|
||||
} from "./src/discovery-shared.js";
|
||||
@@ -12,6 +15,13 @@ type OllamaProviderPlugin = {
|
||||
docsPath: string;
|
||||
envVars: string[];
|
||||
auth: [];
|
||||
resolveSyntheticAuth: (ctx: { providerConfig?: ModelProviderConfig }) =>
|
||||
| {
|
||||
apiKey: string;
|
||||
source: string;
|
||||
mode: "api-key";
|
||||
}
|
||||
| undefined;
|
||||
discovery: {
|
||||
order: "late";
|
||||
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runOllamaDiscovery>;
|
||||
@@ -40,6 +50,16 @@ export const ollamaProviderDiscovery: OllamaProviderPlugin = {
|
||||
docsPath: "/providers/ollama",
|
||||
envVars: ["OLLAMA_API_KEY"],
|
||||
auth: [],
|
||||
resolveSyntheticAuth: ({ providerConfig }) => {
|
||||
if (!hasMeaningfulExplicitOllamaConfig(providerConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
apiKey: OLLAMA_DEFAULT_API_KEY,
|
||||
source: "models.providers.ollama (synthetic local key)",
|
||||
mode: "api-key",
|
||||
};
|
||||
},
|
||||
discovery: {
|
||||
order: "late",
|
||||
run: runOllamaDiscovery,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"id": "xai",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["xai"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
|
||||
27
extensions/xai/provider-discovery.ts
Normal file
27
extensions/xai/provider-discovery.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
|
||||
|
||||
const PROVIDER_ID = "xai";
|
||||
|
||||
function resolveXaiSyntheticAuth(config: unknown) {
|
||||
const apiKey =
|
||||
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]);
|
||||
return apiKey
|
||||
? {
|
||||
apiKey,
|
||||
source: "xAI plugin config",
|
||||
mode: "api-key" as const,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const xaiProviderDiscovery: ProviderPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
label: "xAI",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
resolveSyntheticAuth: ({ config }) => resolveXaiSyntheticAuth(config),
|
||||
};
|
||||
|
||||
export default xaiProviderDiscovery;
|
||||
@@ -14,6 +14,7 @@ export function applyProviderNativeStreamingUsagePolicy(
|
||||
return (
|
||||
applyProviderNativeStreamingUsageCompatWithPlugin({
|
||||
provider: runtimeProviderKey,
|
||||
allowRuntimePluginLoad: false,
|
||||
context: {
|
||||
provider: providerKey,
|
||||
providerConfig: provider,
|
||||
@@ -30,6 +31,7 @@ export function normalizeProviderConfigPolicy(
|
||||
return (
|
||||
normalizeProviderConfigWithPlugin({
|
||||
provider: runtimeProviderKey,
|
||||
allowRuntimePluginLoad: false,
|
||||
context: {
|
||||
provider: providerKey,
|
||||
providerConfig: provider,
|
||||
@@ -46,6 +48,7 @@ export function resolveProviderConfigApiKeyPolicy(
|
||||
return (env) =>
|
||||
resolveProviderConfigApiKeyWithPlugin({
|
||||
provider: runtimeProviderKey,
|
||||
allowRuntimePluginLoad: false,
|
||||
context: {
|
||||
provider: providerKey,
|
||||
env,
|
||||
|
||||
@@ -26,6 +26,22 @@ const PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number>
|
||||
global: 2,
|
||||
workspace: 3,
|
||||
};
|
||||
let providerAuthAliasMapCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, Record<string, string>>
|
||||
>();
|
||||
|
||||
function buildProviderAuthAliasMapCacheKey(params?: ProviderAuthAliasLookupParams): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params?.workspaceDir ?? "",
|
||||
includeUntrustedWorkspacePlugins: params?.includeUntrustedWorkspacePlugins === true,
|
||||
plugins: params?.config?.plugins ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetProviderAuthAliasMapCacheForTest(): void {
|
||||
providerAuthAliasMapCache = new WeakMap<NodeJS.ProcessEnv, Map<string, Record<string, string>>>();
|
||||
}
|
||||
|
||||
function resolveProviderAuthAliasOriginPriority(origin: PluginOrigin | undefined): number {
|
||||
if (!origin) {
|
||||
@@ -83,10 +99,21 @@ function setPreferredAlias(params: {
|
||||
export function resolveProviderAuthAliasMap(
|
||||
params?: ProviderAuthAliasLookupParams,
|
||||
): Record<string, string> {
|
||||
const env = params?.env ?? process.env;
|
||||
const cacheKey = buildProviderAuthAliasMapCacheKey(params);
|
||||
let envCache = providerAuthAliasMapCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, Record<string, string>>();
|
||||
providerAuthAliasMapCache.set(env, envCache);
|
||||
}
|
||||
const cached = envCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const preferredAliases = new Map<string, ProviderAuthAliasCandidate>();
|
||||
@@ -119,6 +146,7 @@ export function resolveProviderAuthAliasMap(
|
||||
for (const [alias, candidate] of preferredAliases) {
|
||||
aliases[alias] = candidate.target;
|
||||
}
|
||||
envCache.set(cacheKey, aliases);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ describe("entry root help fast path", () => {
|
||||
it("prefers precomputed root help text when available", async () => {
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
|
||||
|
||||
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
env: {},
|
||||
});
|
||||
await vi.dynamicImportSettled();
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
@@ -23,20 +22,19 @@ describe("entry root help fast path", () => {
|
||||
it("renders root help without importing the full program", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
|
||||
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
env: {},
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ignores non-root help invocations", () => {
|
||||
it("ignores non-root help invocations", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
|
||||
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], {
|
||||
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], {
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
env: {},
|
||||
});
|
||||
@@ -45,10 +43,10 @@ describe("entry root help fast path", () => {
|
||||
expect(outputRootHelpMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips the host help fast path when a container target is active", () => {
|
||||
it("skips the host help fast path when a container target is active", async () => {
|
||||
const outputRootHelpMock = vi.fn();
|
||||
|
||||
const handled = tryHandleRootHelpFastPath(
|
||||
const handled = await tryHandleRootHelpFastPath(
|
||||
["node", "openclaw", "--container", "demo", "--help"],
|
||||
{
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
|
||||
56
src/entry.ts
56
src/entry.ts
@@ -126,19 +126,19 @@ if (
|
||||
}
|
||||
|
||||
if (!tryHandleRootVersionFastPath(process.argv)) {
|
||||
runMainOrRootHelp(process.argv);
|
||||
await runMainOrRootHelp(process.argv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function tryHandleRootHelpFastPath(
|
||||
export async function tryHandleRootHelpFastPath(
|
||||
argv: string[],
|
||||
deps: {
|
||||
outputRootHelp?: () => void | Promise<void>;
|
||||
onError?: (error: unknown) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
} = {},
|
||||
): boolean {
|
||||
): Promise<boolean> {
|
||||
if (resolveCliContainerTarget(argv, deps.env)) {
|
||||
return false;
|
||||
}
|
||||
@@ -154,35 +154,35 @@ export function tryHandleRootHelpFastPath(
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
if (deps.outputRootHelp) {
|
||||
Promise.resolve()
|
||||
.then(() => deps.outputRootHelp?.())
|
||||
.catch(handleError);
|
||||
return true;
|
||||
}
|
||||
import("./cli/root-help-metadata.js")
|
||||
.then(async ({ outputPrecomputedRootHelpText }) => {
|
||||
if (outputPrecomputedRootHelpText()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (deps.outputRootHelp) {
|
||||
await deps.outputRootHelp();
|
||||
return true;
|
||||
}
|
||||
const { outputPrecomputedRootHelpText } = await import("./cli/root-help-metadata.js");
|
||||
if (!outputPrecomputedRootHelpText()) {
|
||||
const { outputRootHelp } = await import("./cli/program/root-help.js");
|
||||
await outputRootHelp();
|
||||
})
|
||||
.catch(handleError);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function runMainOrRootHelp(argv: string[]): void {
|
||||
if (tryHandleRootHelpFastPath(argv)) {
|
||||
async function runMainOrRootHelp(argv: string[]): Promise<void> {
|
||||
if (await tryHandleRootHelpFastPath(argv)) {
|
||||
return;
|
||||
}
|
||||
import("./cli/run-main.js")
|
||||
.then(({ runCli }) => runCli(argv))
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to start CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
try {
|
||||
const { runCli } = await import("./cli/run-main.js");
|
||||
await runCli(argv);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[openclaw] Failed to start CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ export function resolveProviderPluginsForHooks(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
providerRefs?: string[];
|
||||
applyAutoEnable?: boolean;
|
||||
bundledProviderAllowlistCompat?: boolean;
|
||||
bundledProviderVitestCompat?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
}): ProviderPlugin[] {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
@@ -127,8 +131,10 @@ export function resolveProviderPluginsForHooks(params: {
|
||||
env,
|
||||
activate: false,
|
||||
cache: false,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
applyAutoEnable: params.applyAutoEnable,
|
||||
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
|
||||
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
|
||||
})
|
||||
) {
|
||||
return [];
|
||||
@@ -139,8 +145,10 @@ export function resolveProviderPluginsForHooks(params: {
|
||||
env,
|
||||
activate: false,
|
||||
cache: false,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
applyAutoEnable: params.applyAutoEnable,
|
||||
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
|
||||
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
|
||||
});
|
||||
cacheBucket.set(cacheKey, resolved);
|
||||
return resolved;
|
||||
@@ -151,12 +159,20 @@ export function resolveProviderRuntimePlugin(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
applyAutoEnable?: boolean;
|
||||
bundledProviderAllowlistCompat?: boolean;
|
||||
bundledProviderVitestCompat?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
}): ProviderPlugin | undefined {
|
||||
return resolveProviderPluginsForHooks({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(),
|
||||
env: params.env,
|
||||
providerRefs: [params.provider],
|
||||
applyAutoEnable: params.applyAutoEnable,
|
||||
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat,
|
||||
bundledProviderVitestCompat: params.bundledProviderVitestCompat,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
|
||||
}).find((plugin) => matchesProviderId(plugin, params.provider));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,13 @@ vi.mock("./provider-discovery.runtime.js", () => ({
|
||||
resolvePluginDiscoveryProvidersRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolveCatalogHookProviderPluginIds: vi.fn(() => []),
|
||||
resolveExternalAuthProfileCompatFallbackPluginIds: vi.fn(() => []),
|
||||
resolveExternalAuthProfileProviderPluginIds: vi.fn(() => []),
|
||||
resolveOwningPluginIdsForProvider: vi.fn(() => ["anthropic-vertex"]),
|
||||
}));
|
||||
|
||||
import { resolveProviderSyntheticAuthWithPlugin } from "./provider-runtime.js";
|
||||
|
||||
describe("resolveProviderSyntheticAuthWithPlugin", () => {
|
||||
@@ -53,7 +60,7 @@ describe("resolveProviderSyntheticAuthWithPlugin", () => {
|
||||
source: "gcp-vertex-credentials (ADC)",
|
||||
mode: "api-key",
|
||||
});
|
||||
expect(resolveProviderRuntimePlugin).toHaveBeenCalled();
|
||||
expect(resolveProviderRuntimePlugin).not.toHaveBeenCalled();
|
||||
expect(resolvePluginDiscoveryProvidersRuntime).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,10 @@ type ResolveExternalAuthProfileCompatFallbackPluginIds =
|
||||
typeof import("./providers.js").resolveExternalAuthProfileCompatFallbackPluginIds;
|
||||
type ResolveExternalAuthProfileProviderPluginIds =
|
||||
typeof import("./providers.js").resolveExternalAuthProfileProviderPluginIds;
|
||||
type ResolveOwningPluginIdsForProvider =
|
||||
typeof import("./providers.js").resolveOwningPluginIdsForProvider;
|
||||
type ResolveBundledProviderPolicySurface =
|
||||
typeof import("./provider-public-artifacts.js").resolveBundledProviderPolicySurface;
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn<ResolvePluginProviders>((_) => [] as ProviderPlugin[]);
|
||||
const isPluginProvidersLoadInFlightMock = vi.fn<IsPluginProvidersLoadInFlight>((_) => false);
|
||||
@@ -36,6 +40,12 @@ const resolveExternalAuthProfileCompatFallbackPluginIdsMock =
|
||||
vi.fn<ResolveExternalAuthProfileCompatFallbackPluginIds>((_) => [] as string[]);
|
||||
const resolveExternalAuthProfileProviderPluginIdsMock =
|
||||
vi.fn<ResolveExternalAuthProfileProviderPluginIds>((_) => [] as string[]);
|
||||
const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForProvider>(
|
||||
(_) => undefined,
|
||||
);
|
||||
const resolveBundledProviderPolicySurfaceMock = vi.fn<ResolveBundledProviderPolicySurface>(
|
||||
(_) => null,
|
||||
);
|
||||
const providerRuntimeWarnMock = vi.fn();
|
||||
|
||||
let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins;
|
||||
@@ -244,7 +254,8 @@ describe("provider-runtime", () => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("./provider-public-artifacts.js", () => ({
|
||||
resolveBundledProviderPolicySurface: () => null,
|
||||
resolveBundledProviderPolicySurface: (provider: string) =>
|
||||
resolveBundledProviderPolicySurfaceMock(provider),
|
||||
}));
|
||||
vi.doMock("./providers.js", () => ({
|
||||
resolveCatalogHookProviderPluginIds: (params: unknown) =>
|
||||
@@ -253,6 +264,8 @@ describe("provider-runtime", () => {
|
||||
resolveExternalAuthProfileCompatFallbackPluginIdsMock(params as never),
|
||||
resolveExternalAuthProfileProviderPluginIds: (params: unknown) =>
|
||||
resolveExternalAuthProfileProviderPluginIdsMock(params as never),
|
||||
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
||||
resolveOwningPluginIdsForProviderMock(params as never),
|
||||
}));
|
||||
vi.doMock("./providers.runtime.js", () => ({
|
||||
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
|
||||
@@ -322,6 +335,7 @@ describe("provider-runtime", () => {
|
||||
beforeEach(() => {
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
providerRuntimeTesting.resetExternalAuthFallbackWarningCacheForTest();
|
||||
providerRuntimeTesting.resetCatalogHookProvidersCacheForTest();
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
isPluginProvidersLoadInFlightMock.mockReset();
|
||||
@@ -332,6 +346,10 @@ describe("provider-runtime", () => {
|
||||
resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReturnValue([]);
|
||||
resolveExternalAuthProfileProviderPluginIdsMock.mockReset();
|
||||
resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue([]);
|
||||
resolveOwningPluginIdsForProviderMock.mockReset();
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
|
||||
resolveBundledProviderPolicySurfaceMock.mockReset();
|
||||
resolveBundledProviderPolicySurfaceMock.mockReturnValue(null);
|
||||
providerRuntimeWarnMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -822,6 +840,31 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not scan provider plugins after bundled policy surface handles config", () => {
|
||||
const providerConfig: ModelProviderConfig = {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
};
|
||||
const normalizeConfig = vi.fn(() => providerConfig);
|
||||
resolveBundledProviderPolicySurfaceMock.mockReturnValue({
|
||||
normalizeConfig,
|
||||
});
|
||||
|
||||
expect(
|
||||
normalizeProviderConfigWithPlugin({
|
||||
provider: "openai",
|
||||
context: {
|
||||
provider: "openai",
|
||||
providerConfig,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(normalizeConfig).toHaveBeenCalledTimes(1);
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves provider config defaults through owner plugins", () => {
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
@@ -1758,7 +1801,7 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps cached provider hook results available during a nested provider load", () => {
|
||||
@@ -1825,6 +1868,6 @@ describe("provider-runtime", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(3);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
applyPluginTextReplacements,
|
||||
mergePluginTextTransforms,
|
||||
} from "../agents/plugin-text-transforms.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js";
|
||||
import type { ModelProviderConfig } from "../config/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
resolveCatalogHookProviderPluginIds,
|
||||
resolveExternalAuthProfileCompatFallbackPluginIds,
|
||||
resolveExternalAuthProfileProviderPluginIds,
|
||||
resolveOwningPluginIdsForProvider,
|
||||
} from "./providers.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
import { resolveRuntimeTextTransforms } from "./text-transforms.runtime.js";
|
||||
@@ -83,11 +85,53 @@ import type {
|
||||
|
||||
const log = createSubsystemLogger("plugins/provider-runtime");
|
||||
const warnedExternalAuthFallbackPluginIds = new Set<string>();
|
||||
let catalogHookProvidersCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
|
||||
|
||||
function matchesProviderPluginRef(provider: ProviderPlugin, providerId: string): boolean {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalizeProviderId(provider.id) === normalized) {
|
||||
return true;
|
||||
}
|
||||
return [...(provider.aliases ?? []), ...(provider.hookAliases ?? [])].some(
|
||||
(alias) => normalizeProviderId(alias) === normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function hasExplicitProviderRuntimePluginActivation(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
if (!params.config) {
|
||||
return true;
|
||||
}
|
||||
const ownerPluginIds =
|
||||
resolveOwningPluginIdsForProvider({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}) ?? [];
|
||||
if (ownerPluginIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const allow = new Set(params.config.plugins?.allow ?? []);
|
||||
const entries = params.config.plugins?.entries ?? {};
|
||||
return ownerPluginIds.some((pluginId) => allow.has(pluginId) || entries[pluginId] !== undefined);
|
||||
}
|
||||
|
||||
function resetExternalAuthFallbackWarningCacheForTest(): void {
|
||||
warnedExternalAuthFallbackPluginIds.clear();
|
||||
}
|
||||
|
||||
function resetCatalogHookProvidersCacheForTest(): void {
|
||||
catalogHookProvidersCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
|
||||
}
|
||||
|
||||
export {
|
||||
clearProviderRuntimeHookCache,
|
||||
prepareProviderExtraParams,
|
||||
@@ -102,6 +146,7 @@ export {
|
||||
export const __testing = {
|
||||
...providerHookRuntimeTesting,
|
||||
resetExternalAuthFallbackWarningCacheForTest,
|
||||
resetCatalogHookProvidersCacheForTest,
|
||||
} as const;
|
||||
|
||||
function resolveProviderPluginsForCatalogHooks(params: {
|
||||
@@ -110,19 +155,37 @@ function resolveProviderPluginsForCatalogHooks(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
const env = params.env ?? process.env;
|
||||
let envCache = catalogHookProvidersCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, ProviderPlugin[]>();
|
||||
catalogHookProvidersCache.set(env, envCache);
|
||||
}
|
||||
const cacheKey = JSON.stringify({
|
||||
workspaceDir: workspaceDir ?? "",
|
||||
plugins: params.config?.plugins ?? null,
|
||||
});
|
||||
const cached = envCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const onlyPluginIds = resolveCatalogHookProviderPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env: params.env,
|
||||
env,
|
||||
});
|
||||
if (onlyPluginIds.length === 0) {
|
||||
envCache.set(cacheKey, []);
|
||||
return [];
|
||||
}
|
||||
return resolveProviderPluginsForHooks({
|
||||
const providers = resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
workspaceDir,
|
||||
env,
|
||||
onlyPluginIds,
|
||||
});
|
||||
envCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function runProviderDynamicModel(params: {
|
||||
@@ -410,6 +473,7 @@ export function normalizeProviderConfigWithPlugin(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderNormalizeConfigContext;
|
||||
allowRuntimePluginLoad?: boolean;
|
||||
}): ModelProviderConfig | undefined {
|
||||
const hasConfigChange = (normalized: ModelProviderConfig) =>
|
||||
normalized !== params.context.providerConfig;
|
||||
@@ -418,23 +482,15 @@ export function normalizeProviderConfigWithPlugin(params: {
|
||||
const normalized = bundledSurface.normalizeConfig(params.context);
|
||||
return normalized && hasConfigChange(normalized) ? normalized : undefined;
|
||||
}
|
||||
const matchedPlugin = resolveProviderHookPlugin(params);
|
||||
if (!hasExplicitProviderRuntimePluginActivation(params)) {
|
||||
return undefined;
|
||||
}
|
||||
if (params.allowRuntimePluginLoad === false) {
|
||||
return undefined;
|
||||
}
|
||||
const matchedPlugin = resolveProviderRuntimePlugin(params);
|
||||
const normalizedMatched = matchedPlugin?.normalizeConfig?.(params.context);
|
||||
if (normalizedMatched && hasConfigChange(normalizedMatched)) {
|
||||
return normalizedMatched;
|
||||
}
|
||||
|
||||
for (const candidate of resolveProviderPluginsForHooks(params)) {
|
||||
if (!candidate.normalizeConfig || candidate === matchedPlugin) {
|
||||
continue;
|
||||
}
|
||||
const normalized = candidate.normalizeConfig(params.context);
|
||||
if (normalized && hasConfigChange(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return normalizedMatched && hasConfigChange(normalizedMatched) ? normalizedMatched : undefined;
|
||||
}
|
||||
|
||||
export function applyProviderNativeStreamingUsageCompatWithPlugin(params: {
|
||||
@@ -443,9 +499,13 @@ export function applyProviderNativeStreamingUsageCompatWithPlugin(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderNormalizeConfigContext;
|
||||
allowRuntimePluginLoad?: boolean;
|
||||
}): ModelProviderConfig | undefined {
|
||||
if (params.allowRuntimePluginLoad === false) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
resolveProviderHookPlugin(params)?.applyNativeStreamingUsageCompat?.(params.context) ??
|
||||
resolveProviderRuntimePlugin(params)?.applyNativeStreamingUsageCompat?.(params.context) ??
|
||||
undefined
|
||||
);
|
||||
}
|
||||
@@ -456,13 +516,17 @@ export function resolveProviderConfigApiKeyWithPlugin(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderResolveConfigApiKeyContext;
|
||||
allowRuntimePluginLoad?: boolean;
|
||||
}): string | undefined {
|
||||
const bundledSurface = resolveBundledProviderPolicySurface(params.provider);
|
||||
if (bundledSurface?.resolveConfigApiKey) {
|
||||
return normalizeOptionalString(bundledSurface.resolveConfigApiKey(params.context));
|
||||
}
|
||||
if (params.allowRuntimePluginLoad === false) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeOptionalString(
|
||||
resolveProviderHookPlugin(params)?.resolveConfigApiKey?.(params.context),
|
||||
resolveProviderRuntimePlugin(params)?.resolveConfigApiKey?.(params.context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -775,9 +839,34 @@ export function resolveProviderSyntheticAuthWithPlugin(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderResolveSyntheticAuthContext;
|
||||
}) {
|
||||
const runtimeResolved = resolveProviderRuntimePlugin(params)?.resolveSyntheticAuth?.(
|
||||
params.context,
|
||||
);
|
||||
const discoveryPluginIds =
|
||||
resolveOwningPluginIdsForProvider({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}) ?? [];
|
||||
const discoveryProvider = (
|
||||
discoveryPluginIds.length > 0
|
||||
? resolvePluginDiscoveryProvidersRuntime({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
onlyPluginIds: discoveryPluginIds,
|
||||
discoveryEntriesOnly: true,
|
||||
})
|
||||
: []
|
||||
).find((provider) => matchesProviderPluginRef(provider, params.provider));
|
||||
if (typeof discoveryProvider?.resolveSyntheticAuth === "function") {
|
||||
return discoveryProvider.resolveSyntheticAuth(params.context) ?? undefined;
|
||||
}
|
||||
const runtimeResolved = resolveProviderRuntimePlugin({
|
||||
...params,
|
||||
applyAutoEnable: false,
|
||||
bundledProviderAllowlistCompat: false,
|
||||
bundledProviderVitestCompat: false,
|
||||
installBundledRuntimeDeps: false,
|
||||
})?.resolveSyntheticAuth?.(params.context);
|
||||
if (runtimeResolved) {
|
||||
return runtimeResolved;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ function resolveRuntimeProviderPluginLoadState(
|
||||
env: base.env,
|
||||
workspaceDir: base.workspaceDir,
|
||||
onlyPluginIds: runtimeRequestedPluginIds,
|
||||
applyAutoEnable: true,
|
||||
applyAutoEnable: params.applyAutoEnable ?? true,
|
||||
compatMode: {
|
||||
allowlist: params.bundledProviderAllowlistCompat,
|
||||
enablement: "allowlist",
|
||||
@@ -233,6 +233,7 @@ function resolveRuntimeProviderPluginLoadState(
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
cache: params.cache ?? true,
|
||||
activate: params.activate ?? false,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
|
||||
},
|
||||
);
|
||||
return { loadOptions };
|
||||
@@ -264,6 +265,8 @@ export function resolvePluginProviders(params: {
|
||||
modelRefs?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
applyAutoEnable?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"];
|
||||
mode?: "runtime" | "setup";
|
||||
includeUntrustedWorkspacePlugins?: boolean;
|
||||
|
||||
@@ -426,6 +426,30 @@ function dedupeSortedPluginIds(values: Iterable<string>): string[] {
|
||||
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
let owningProviderPluginIdsCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, string[] | undefined>
|
||||
>();
|
||||
|
||||
function buildOwningProviderPluginIdsCacheKey(params: {
|
||||
provider: string;
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
provider: normalizeProviderId(params.provider),
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
plugins: params.config?.plugins ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetProviderOwnerPluginIdsCacheForTest(): void {
|
||||
owningProviderPluginIdsCache = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, string[] | undefined>
|
||||
>();
|
||||
}
|
||||
|
||||
function resolvePreferredManifestPluginIds(
|
||||
registry: PluginManifestRegistry,
|
||||
matchedPluginIds: readonly string[],
|
||||
@@ -478,18 +502,33 @@ export function resolveOwningPluginIdsForProvider(params: {
|
||||
return pluginIds.length > 0 ? pluginIds : undefined;
|
||||
}
|
||||
|
||||
const env = params.env ?? process.env;
|
||||
let envCache = owningProviderPluginIdsCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, string[] | undefined>();
|
||||
owningProviderPluginIdsCache.set(env, envCache);
|
||||
}
|
||||
const cacheKey = buildOwningProviderPluginIdsCacheKey({
|
||||
provider: normalizedProvider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
if (envCache.has(cacheKey)) {
|
||||
return envCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const pluginIds = [
|
||||
...resolveProviderOwners({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
env,
|
||||
providerId: normalizedProvider,
|
||||
includeDisabled: true,
|
||||
}),
|
||||
...resolvePluginContributionOwners({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
env,
|
||||
contribution: "cliBackends",
|
||||
matches: (backendId) => normalizeProviderId(backendId) === normalizedProvider,
|
||||
includeDisabled: true,
|
||||
@@ -497,7 +536,9 @@ export function resolveOwningPluginIdsForProvider(params: {
|
||||
];
|
||||
|
||||
const deduped = dedupeSortedPluginIds(pluginIds);
|
||||
return deduped.length > 0 ? deduped : undefined;
|
||||
const resolved = deduped.length > 0 ? deduped : undefined;
|
||||
envCache.set(cacheKey, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function resolveOwningPluginIdsForModelRef(params: {
|
||||
|
||||
Reference in New Issue
Block a user