fix(amazon-bedrock): refresh live discovery and guardrail config

This commit is contained in:
Vincent Koc
2026-04-22 23:41:27 -07:00
parent d8db122a23
commit da8993203c
3 changed files with 150 additions and 11 deletions

View File

@@ -113,6 +113,7 @@ Docs: https://docs.openclaw.ai
- GitHub Copilot: re-read plugin discovery config from the live runtime snapshot, so toggling `plugins.entries.github-copilot.config.discovery.enabled` takes effect without a restart. Thanks @vincentkoc.
- Ollama: re-read plugin discovery config from the live runtime snapshot, so toggling `plugins.entries.ollama.config.discovery.enabled` takes effect without a restart. Thanks @vincentkoc.
- OpenAI: re-read the plugin prompt-overlay personality from live runtime config, so GPT-5 system prompt contributions update without a restart when `plugins.entries.openai.config.personality` changes. Thanks @vincentkoc.
- Amazon Bedrock: re-read live discovery and guardrail plugin config, so toggling `plugins.entries.amazon-bedrock.config.discovery` or `plugins.entries.amazon-bedrock.config.guardrail` takes effect without a restart. Thanks @vincentkoc.
- Agents/subagents: drop bare `NO_REPLY` from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana.
- Plugins/install: keep bundled plugin dependencies off npm install while repairing them when plugins activate from a packaged install, including Feishu/Lark, Browser, and direct bundled channel setup-entry loads.
- CLI/channels: skip and cache bundled channel plugin, setup, and secrets load failures during read-only discovery, so one broken unused bundled channel cannot crash `openclaw status` or bootstrap secret scans.

View File

@@ -7,9 +7,15 @@ import type { PluginRuntime } from "../../src/plugins/runtime/types.js";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
import amazonBedrockPlugin from "./index.js";
type InferenceProfileResult = { models?: Array<{ modelArn?: string }> } | Error;
type BedrockClientResult =
| {
models?: Array<{ modelArn?: string }>;
modelSummaries?: Array<Record<string, unknown>>;
inferenceProfileSummaries?: Array<Record<string, unknown>>;
}
| Error;
const inferenceProfileResults: InferenceProfileResult[] = [];
const inferenceProfileResults: BedrockClientResult[] = [];
const bedrockClientConfigs: Array<Record<string, unknown>> = [];
const sendGetInferenceProfile = vi.fn(async () => {
const next = inferenceProfileResults.shift();
@@ -24,6 +30,14 @@ vi.mock("@aws-sdk/client-bedrock", () => {
constructor(readonly input: { inferenceProfileIdentifier: string }) {}
}
class ListFoundationModelsCommand {
constructor(readonly input: Record<string, unknown> = {}) {}
}
class ListInferenceProfilesCommand {
constructor(readonly input: Record<string, unknown> = {}) {}
}
class BedrockClient {
constructor(config: Record<string, unknown> = {}) {
bedrockClientConfigs.push(config);
@@ -35,6 +49,8 @@ vi.mock("@aws-sdk/client-bedrock", () => {
return {
BedrockClient,
GetInferenceProfileCommand,
ListFoundationModelsCommand,
ListInferenceProfilesCommand,
};
});
@@ -113,10 +129,12 @@ function callWrappedStream(
provider: RegisteredProviderPlugin,
modelId: string,
modelDescriptor: never,
config?: OpenClawConfig,
): Record<string, unknown> {
const wrapped = provider.wrapStreamFn?.({
provider: "amazon-bedrock",
modelId,
config,
streamFn: spyStreamFn,
} as never);
@@ -138,6 +156,31 @@ function callWrappedStream(
return result;
}
async function runCatalog(
provider: RegisteredProviderPlugin,
config: OpenClawConfig,
env: NodeJS.ProcessEnv = {} as NodeJS.ProcessEnv,
) {
return provider.catalog?.run({
config,
env,
} as never);
}
function runtimePluginConfig(config?: Record<string, unknown>): OpenClawConfig {
return {
plugins: {
entries: config
? {
"amazon-bedrock": {
config,
},
}
: {},
},
} as OpenClawConfig;
}
describe("amazon-bedrock provider plugin", () => {
beforeEach(() => {
inferenceProfileResults.length = 0;
@@ -354,6 +397,91 @@ describe("amazon-bedrock provider plugin", () => {
// Non-Anthropic models should also get cacheRetention: "none"
expect(result).toMatchObject({ cacheRetention: "none" });
});
it("uses live plugin config to inject guardrailConfig after startup disable", async () => {
const provider = await registerWithConfig(undefined);
const result = callWrappedStream(
provider,
NON_ANTHROPIC_MODEL,
MODEL_DESCRIPTOR,
runtimePluginConfig({
guardrail: {
guardrailIdentifier: "live-guardrail",
guardrailVersion: "7",
},
}),
);
expect(result._capturedPayload).toEqual({
guardrailConfig: {
guardrailIdentifier: "live-guardrail",
guardrailVersion: "7",
},
});
});
it("does not revive startup guardrail config when the live plugin entry is removed", async () => {
const provider = await registerWithConfig({
guardrail: {
guardrailIdentifier: "startup-guardrail",
guardrailVersion: "5",
},
});
const result = callWrappedStream(
provider,
NON_ANTHROPIC_MODEL,
MODEL_DESCRIPTOR,
runtimePluginConfig(undefined),
);
expect(result).not.toHaveProperty("_capturedPayload");
expect(result).toMatchObject({ cacheRetention: "none" });
});
});
describe("discovery config", () => {
it("uses live plugin config to re-enable discovery after startup disable", async () => {
inferenceProfileResults.push(
{
modelSummaries: [
{
modelId: NON_ANTHROPIC_MODEL,
modelName: "Nova Micro",
providerName: "Amazon",
inputModalities: ["TEXT"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
},
{
inferenceProfileSummaries: [],
},
);
const provider = await registerWithConfig({
discovery: {
enabled: false,
},
});
const catalog = await runCatalog(
provider,
runtimePluginConfig({
discovery: {
enabled: true,
region: "us-east-1",
},
}),
);
expect(catalog).toMatchObject({
provider: {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
},
});
});
});
describe("application inference profile cache point injection", () => {

View File

@@ -1,4 +1,5 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import {
ANTHROPIC_BY_MODEL_REPLAY_HOOKS,
@@ -249,8 +250,17 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
] as const;
const anthropicByModelReplayHooks = ANTHROPIC_BY_MODEL_REPLAY_HOOKS;
const pluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig;
const guardrail = pluginConfig.guardrail;
const startupPluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig;
function resolveCurrentPluginConfig(
config: { plugins?: { entries?: Record<string, unknown> } } | undefined,
): AmazonBedrockPluginConfig | undefined {
const runtimePluginConfig = resolvePluginConfigObject(config, providerId);
return (
(runtimePluginConfig as AmazonBedrockPluginConfig | undefined) ??
(config ? undefined : startupPluginConfig)
);
}
api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter);
@@ -266,11 +276,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
return createBedrockNoCacheWrapper(streamFn);
};
const cacheWrapStreamFn =
guardrail?.guardrailIdentifier && guardrail?.guardrailVersion
? createGuardrailWrapStreamFn(baseWrapStreamFn, guardrail)
: baseWrapStreamFn;
/** Extract the AWS region from a bedrock-runtime baseUrl. */
function extractRegionFromBaseUrl(baseUrl: string | undefined): string | undefined {
if (!baseUrl) {
@@ -321,9 +326,10 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
catalog: {
order: "simple",
run: async (ctx) => {
const currentPluginConfig = resolveCurrentPluginConfig(ctx.config);
const implicit = await resolveImplicitBedrockProvider({
config: ctx.config,
pluginConfig,
pluginConfig: currentPluginConfig,
env: ctx.env,
});
if (!implicit) {
@@ -340,8 +346,12 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
...anthropicByModelReplayHooks,
wrapStreamFn: ({ modelId, config, model, streamFn }) => {
const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail;
// Apply cache + guardrail wrapping.
const wrapped = cacheWrapStreamFn({ modelId, streamFn });
const wrapped =
currentGuardrail?.guardrailIdentifier && currentGuardrail?.guardrailVersion
? createGuardrailWrapStreamFn(baseWrapStreamFn, currentGuardrail)({ modelId, streamFn })
: baseWrapStreamFn({ modelId, streamFn });
const region = resolveBedrockRegion(config) ?? extractRegionFromBaseUrl(model?.baseUrl);
const mayNeedCacheInjection =
isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId);