From 503a3aa125c801fa0a43a82d9d0953f09a227e4d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 06:24:20 +0100 Subject: [PATCH] fix: defer bedrock discovery sdk import --- CHANGELOG.md | 1 + extensions/amazon-bedrock/api.ts | 3 +- extensions/amazon-bedrock/discovery-shared.ts | 28 +++++++ extensions/amazon-bedrock/discovery.ts | 83 +++++++++++-------- extensions/amazon-bedrock/lazy-import.test.ts | 56 +++++++++++++ .../amazon-bedrock/register.sync.runtime.ts | 7 +- extensions/amazon-bedrock/setup-api.ts | 2 +- 7 files changed, 136 insertions(+), 44 deletions(-) create mode 100644 extensions/amazon-bedrock/discovery-shared.ts create mode 100644 extensions/amazon-bedrock/lazy-import.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0595919c2af..bd7fcc424fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/amazon-bedrock/api.ts b/extensions/amazon-bedrock/api.ts index 880380f970c..07d12f52809 100644 --- a/extensions/amazon-bedrock/api.ts +++ b/extensions/amazon-bedrock/api.ts @@ -1,7 +1,6 @@ +export { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js"; export { discoverBedrockModels, - mergeImplicitBedrockProvider, resetBedrockDiscoveryCacheForTest, - resolveBedrockConfigApiKey, resolveImplicitBedrockProvider, } from "./discovery.js"; diff --git a/extensions/amazon-bedrock/discovery-shared.ts b/extensions/amazon-bedrock/discovery-shared.ts new file mode 100644 index 00000000000..4ad654b3bcf --- /dev/null +++ b/extensions/amazon-bedrock/discovery-shared.ts @@ -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, + }; +} diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index aaf96a1fa7f..b71a8d6767a 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -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 { + 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 = {}) {} + } + class ListInferenceProfilesCommand { + constructor(readonly input: Record = {}) {} + } + 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 { 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(); @@ -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, - }; -} diff --git a/extensions/amazon-bedrock/lazy-import.test.ts b/extensions/amazon-bedrock/lazy-import.test.ts new file mode 100644 index 00000000000..312144b8d48 --- /dev/null +++ b/extensions/amazon-bedrock/lazy-import.test.ts @@ -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); + }); +}); diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 962afdc947e..ed7535c6173 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -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, diff --git a/extensions/amazon-bedrock/setup-api.ts b/extensions/amazon-bedrock/setup-api.ts index ebc4b64f41a..fd2f4443620 100644 --- a/extensions/amazon-bedrock/setup-api.ts +++ b/extensions/amazon-bedrock/setup-api.ts @@ -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",