fix: defer bedrock discovery sdk import

This commit is contained in:
Peter Steinberger
2026-04-26 06:24:20 +01:00
parent 9f4b155c47
commit 503a3aa125
7 changed files with 136 additions and 44 deletions

View File

@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
- Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin.
- Gateway/Linux: include fnm `aliases/default/bin` in generated service PATHs and let doctor accept either modern fnm aliases or the legacy `current/bin` symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott.
- Installer/Linux: run apt installs with noninteractive dpkg and needrestart settings so fresh Ubuntu 24.04 `curl | bash` installs do not hang while installing Node.js, Git, or build tools. Fixes #41146. Thanks @iht76, @alexcarv318, @cs3gallery, @firofame, and @cgdusek.
- Providers/Bedrock: defer the AWS SDK import until Bedrock discovery actually runs so plugin registration and setup stay lightweight on cold start. Fixes #71690. Thanks @jarvis-ai-gregmoser.
- WhatsApp: remove ack reactions after a visible reply when `messages.removeAckAfterReply` is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken.
- Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo.
- Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.

View File

@@ -1,7 +1,6 @@
export { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js";
export {
discoverBedrockModels,
mergeImplicitBedrockProvider,
resetBedrockDiscoveryCacheForTest,
resolveBedrockConfigApiKey,
resolveImplicitBedrockProvider,
} from "./discovery.js";

View File

@@ -0,0 +1,28 @@
import { resolveAwsSdkEnvVarName } from "openclaw/plugin-sdk/provider-auth-runtime";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
export function resolveBedrockConfigApiKey(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
// When no AWS auth env marker is present, Bedrock should fall back to the
// AWS SDK default credential chain instead of persisting a fake apiKey marker.
return resolveAwsSdkEnvVarName(env);
}
export function mergeImplicitBedrockProvider(params: {
existing: ModelProviderConfig | undefined;
implicit: ModelProviderConfig;
}): ModelProviderConfig {
const { existing, implicit } = params;
if (!existing) {
return implicit;
}
return {
...implicit,
...existing,
models:
Array.isArray(existing.models) && existing.models.length > 0
? existing.models
: implicit.models,
};
}

View File

@@ -1,13 +1,10 @@
import {
BedrockClient,
ListFoundationModelsCommand,
type BedrockClient,
type ListFoundationModelsCommandOutput,
ListInferenceProfilesCommand,
type ListInferenceProfilesCommandOutput,
} from "@aws-sdk/client-bedrock";
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolveAwsSdkEnvVarName } from "openclaw/plugin-sdk/provider-auth-runtime";
import type {
BedrockDiscoveryConfig,
ModelDefinitionConfig,
@@ -17,6 +14,7 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/text-runtime";
import { resolveBedrockConfigApiKey } from "./discovery-shared.js";
const log = createSubsystemLogger("bedrock-discovery");
@@ -149,6 +147,38 @@ type InferenceProfileSummary = NonNullable<
ListInferenceProfilesCommandOutput["inferenceProfileSummaries"]
>[number];
type BedrockDiscoverySdk = {
createClient(region: string): BedrockClient;
createListFoundationModelsCommand(): unknown;
createListInferenceProfilesCommand(input: { nextToken?: string }): unknown;
};
async function loadBedrockDiscoverySdk(): Promise<BedrockDiscoverySdk> {
const { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } =
await import("@aws-sdk/client-bedrock");
return {
createClient: (region) => new BedrockClient({ region }),
createListFoundationModelsCommand: () => new ListFoundationModelsCommand({}),
createListInferenceProfilesCommand: (input) => new ListInferenceProfilesCommand(input),
};
}
function createInjectedClientDiscoverySdk(): BedrockDiscoverySdk {
class ListFoundationModelsCommand {
constructor(readonly input: Record<string, unknown> = {}) {}
}
class ListInferenceProfilesCommand {
constructor(readonly input: Record<string, unknown> = {}) {}
}
return {
createClient() {
throw new Error("clientFactory is required for injected Bedrock discovery commands");
},
createListFoundationModelsCommand: () => new ListFoundationModelsCommand({}),
createListInferenceProfilesCommand: (input) => new ListInferenceProfilesCommand(input),
};
}
type BedrockDiscoveryCacheEntry = {
expiresAt: number;
value?: ModelDefinitionConfig[];
@@ -320,13 +350,14 @@ function resolveBaseModelId(profile: InferenceProfileSummary): string | undefine
*/
async function fetchInferenceProfileSummaries(
client: BedrockClient,
createListInferenceProfilesCommand: BedrockDiscoverySdk["createListInferenceProfilesCommand"],
): Promise<InferenceProfileSummary[]> {
try {
const profiles: InferenceProfileSummary[] = [];
let nextToken: string | undefined;
do {
const response: ListInferenceProfilesCommandOutput = await client.send(
new ListInferenceProfilesCommand({ nextToken }),
createListInferenceProfilesCommand({ nextToken }) as never,
);
for (const summary of response.inferenceProfileSummaries ?? []) {
profiles.push(summary);
@@ -414,14 +445,6 @@ export function resetBedrockDiscoveryCacheForTest(): void {
hasLoggedBedrockError = false;
}
export function resolveBedrockConfigApiKey(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
// When no AWS auth env marker is present, Bedrock should fall back to the
// AWS SDK default credential chain instead of persisting a fake apiKey marker.
return resolveAwsSdkEnvVarName(env);
}
export async function discoverBedrockModels(params: {
region: string;
config?: BedrockDiscoveryConfig;
@@ -454,7 +477,10 @@ export async function discoverBedrockModels(params: {
}
}
const clientFactory = params.clientFactory ?? ((region: string) => new BedrockClient({ region }));
const sdk = params.clientFactory
? createInjectedClientDiscoverySdk()
: await loadBedrockDiscoverySdk();
const clientFactory = params.clientFactory ?? ((region: string) => sdk.createClient(region));
const client = clientFactory(params.region);
const discoveryPromise = (async () => {
@@ -462,10 +488,13 @@ export async function discoverBedrockModels(params: {
// Both API calls are independent, but we need the foundation model data
// to resolve inference profile capabilities — so we fetch in parallel,
// then build the lookup map before processing profiles.
const [foundationResponse, profileSummaries] = await Promise.all([
client.send(new ListFoundationModelsCommand({})),
fetchInferenceProfileSummaries(client),
const [rawFoundationResponse, profileSummaries] = await Promise.all([
client.send(sdk.createListFoundationModelsCommand() as never),
fetchInferenceProfileSummaries(client, (input) =>
sdk.createListInferenceProfilesCommand(input),
),
]);
const foundationResponse = rawFoundationResponse as ListFoundationModelsCommandOutput;
const discovered: ModelDefinitionConfig[] = [];
const seenIds = new Set<string>();
@@ -556,7 +585,7 @@ export async function resolveImplicitBedrockProvider(params: {
...params.pluginConfig?.discovery,
};
const enabled = discoveryConfig?.enabled;
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
const hasAwsCreds = resolveBedrockConfigApiKey(env) !== undefined;
if (enabled === false) {
return null;
}
@@ -581,21 +610,3 @@ export async function resolveImplicitBedrockProvider(params: {
models,
};
}
export function mergeImplicitBedrockProvider(params: {
existing: ModelProviderConfig | undefined;
implicit: ModelProviderConfig;
}): ModelProviderConfig {
const { existing, implicit } = params;
if (!existing) {
return implicit;
}
return {
...implicit,
...existing,
models:
Array.isArray(existing.models) && existing.models.length > 0
? existing.models
: implicit.models,
};
}

View File

@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
function mockBedrockSdkImportTripwire(): () => number {
let importCount = 0;
vi.doMock("@aws-sdk/client-bedrock", () => {
importCount += 1;
throw new Error("Bedrock SDK should not load during plugin registration");
});
return () => importCount;
}
describe("amazon-bedrock lazy imports", () => {
afterEach(() => {
vi.doUnmock("@aws-sdk/client-bedrock");
vi.resetModules();
});
it("registers the runtime plugin without loading the Bedrock SDK", async () => {
const getImportCount = mockBedrockSdkImportTripwire();
const { default: amazonBedrockPlugin } = await import("./index.js");
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
expect(provider.id).toBe("amazon-bedrock");
expect(provider.resolveConfigApiKey?.({ env: { AWS_PROFILE: "default" } } as never)).toBe(
"AWS_PROFILE",
);
expect(getImportCount()).toBe(0);
});
it("registers the setup entry without loading the Bedrock SDK", async () => {
const getImportCount = mockBedrockSdkImportTripwire();
const { default: setupPlugin } = await import("./setup-api.js");
const providers: Array<{
id: string;
resolveConfigApiKey?: (params: never) => string | undefined;
}> = [];
setupPlugin.register({
registerProvider(provider: {
id: string;
resolveConfigApiKey?: (params: never) => string | undefined;
}) {
providers.push(provider);
},
registerConfigMigration() {},
} as never);
expect(providers.map((provider) => provider.id)).toEqual(["amazon-bedrock"]);
expect(providers[0]?.resolveConfigApiKey?.({ env: { AWS_PROFILE: "default" } } as never)).toBe(
"AWS_PROFILE",
);
expect(getImportCount()).toBe(0);
});
});

View File

@@ -10,11 +10,7 @@ import {
isAnthropicBedrockModel,
streamWithPayloadPatch,
} from "openclaw/plugin-sdk/provider-stream-shared";
import {
mergeImplicitBedrockProvider,
resolveBedrockConfigApiKey,
resolveImplicitBedrockProvider,
} from "./api.js";
import { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js";
import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
type GuardrailConfig = {
@@ -330,6 +326,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
catalog: {
order: "simple",
run: async (ctx) => {
const { resolveImplicitBedrockProvider } = await import("./discovery.js");
const currentPluginConfig = resolveCurrentPluginConfig(ctx.config);
const implicit = await resolveImplicitBedrockProvider({
config: ctx.config,

View File

@@ -1,6 +1,6 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { migrateAmazonBedrockLegacyConfig } from "./config-api.js";
import { resolveBedrockConfigApiKey } from "./discovery.js";
import { resolveBedrockConfigApiKey } from "./discovery-shared.js";
export default definePluginEntry({
id: "amazon-bedrock",