mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
fix: defer bedrock discovery sdk import
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js";
|
||||
export {
|
||||
discoverBedrockModels,
|
||||
mergeImplicitBedrockProvider,
|
||||
resetBedrockDiscoveryCacheForTest,
|
||||
resolveBedrockConfigApiKey,
|
||||
resolveImplicitBedrockProvider,
|
||||
} from "./discovery.js";
|
||||
|
||||
28
extensions/amazon-bedrock/discovery-shared.ts
Normal file
28
extensions/amazon-bedrock/discovery-shared.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
56
extensions/amazon-bedrock/lazy-import.test.ts
Normal file
56
extensions/amazon-bedrock/lazy-import.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user