diff --git a/CHANGELOG.md b/CHANGELOG.md
index 460dd2ad7f8..0b257f2fbe8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` and `google-gemini-cli/gemini-3-pro-preview` selections to `google/gemini-3.1-pro-preview` before they are written to model config.
+- Amazon Bedrock: support `serviceTier` parameter for Bedrock models, configurable via `agents.defaults.params.serviceTier` or per-model in `agents.defaults.models`. Valid values: `default`, `flex`, `priority`, `reserved`. (#64512) Thanks @mobilinkd.
- Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack.
- Control UI/usage: add transcript-backed historical lineage rollups for rotated logical sessions, with current-instance vs historical-lineage scope controls and long-range presets so usage history stays visible after restarts and updates. Fixes #50701. Thanks @dev-gideon-llc and @BunsDev.
- Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev.
diff --git a/docs/providers/bedrock.md b/docs/providers/bedrock.md
index 7f2f6b8f0cc..6257e0c05fd 100644
--- a/docs/providers/bedrock.md
+++ b/docs/providers/bedrock.md
@@ -256,6 +256,49 @@ openclaw models list
+
+ Some Bedrock models support a `service_tier` parameter to optimize for cost
+ or latency. The following tiers are available:
+
+ | Tier | Description |
+ |------|-------------|
+ | `default` | Standard Bedrock tier |
+ | `flex` | Discounted processing for workloads that can tolerate longer latency |
+ | `priority` | Prioritized processing for latency-sensitive workloads |
+ | `reserved` | Reserved capacity for steady-state workloads |
+
+ Set `serviceTier` (or `service_tier`) via `agents.defaults.params` for
+ Bedrock model requests, or per-model in
+ `agents.defaults.models[""].params`:
+
+ ```json5
+ {
+ agents: {
+ defaults: {
+ params: {
+ serviceTier: "flex", // applies to all models
+ },
+ models: {
+ "amazon-bedrock/mistral.mistral-large-3-675b-instruct": {
+ params: {
+ serviceTier: "priority", // per-model override
+ },
+ },
+ },
+ },
+ },
+ }
+ ```
+
+ Valid values are `default`, `flex`, `priority`, and `reserved`. Not all
+ models support all tiers — if an unsupported tier is requested, Bedrock will
+ return a validation error. Note: the error message is somewhat misleading;
+ it may say "The provided model identifier is invalid" rather than indicating
+ an unsupported service tier. If you see this error, check whether the model
+ supports the requested tier.
+
+
+
Bedrock rejects the `temperature` parameter for Claude Opus 4.7. OpenClaw
omits `temperature` automatically for any Opus 4.7 Bedrock ref, including
diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts
index 4e8666bbb54..240b78a09b9 100644
--- a/extensions/amazon-bedrock/index.test.ts
+++ b/extensions/amazon-bedrock/index.test.ts
@@ -160,34 +160,28 @@ function makeAppInferenceProfileDescriptor(modelId: string): never {
} as never;
}
-/**
- * Call wrapStreamFn and then invoke the returned stream function, capturing
- * the payload via the onPayload hook that streamWithPayloadPatch installs.
- */
async function callWrappedStream(
provider: RegisteredProviderPlugin,
modelId: string,
modelDescriptor: never,
config?: OpenClawConfig,
+ extraParams?: Record,
+ payload: Record = {},
): Promise> {
const wrapped = provider.wrapStreamFn?.({
provider: "amazon-bedrock",
modelId,
config,
streamFn: spyStreamFn,
+ ...(extraParams ? { extraParams } : {}),
} as never);
- // The wrapped stream returns the options object (from spyStreamFn).
- // For guardrail-wrapped streams, streamWithPayloadPatch intercepts onPayload,
- // so we need to invoke onPayload on the returned options to trigger the patch.
const result = wrapped?.(modelDescriptor, { messages: [] } as never, {}) as unknown as Record<
string,
unknown
>;
- // If onPayload was installed by streamWithPayloadPatch, call it to apply the patch.
if (typeof result?.onPayload === "function") {
- const payload: Record = {};
await (result.onPayload as (p: Record, model: unknown) => Promise)(
payload,
modelDescriptor,
@@ -719,6 +713,89 @@ describe("amazon-bedrock provider plugin", () => {
});
});
+ describe("service tier", () => {
+ const CONVERSE_MODEL_DESCRIPTOR = {
+ api: "bedrock-converse-stream",
+ provider: "amazon-bedrock",
+ id: NON_ANTHROPIC_MODEL,
+ } as never;
+
+ it("injects serviceTier for valid camelCase value ('flex')", async () => {
+ const provider = await registerWithConfig(undefined);
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ CONVERSE_MODEL_DESCRIPTOR,
+ runtimePluginConfig(undefined),
+ { serviceTier: "flex" },
+ );
+ expect(result._capturedPayload).toMatchObject({ serviceTier: { type: "flex" } });
+ });
+
+ it("injects serviceTier for valid snake_case value ('priority')", async () => {
+ const provider = await registerWithConfig(undefined);
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ CONVERSE_MODEL_DESCRIPTOR,
+ runtimePluginConfig(undefined),
+ { service_tier: "priority" },
+ );
+ expect(result._capturedPayload).toMatchObject({ serviceTier: { type: "priority" } });
+ });
+
+ it("injects serviceTier for all valid tier names", async () => {
+ const provider = await registerWithConfig(undefined);
+ for (const tier of ["flex", "priority", "default", "reserved"] as const) {
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ CONVERSE_MODEL_DESCRIPTOR,
+ runtimePluginConfig(undefined),
+ { serviceTier: tier },
+ );
+ expect(result._capturedPayload).toMatchObject({ serviceTier: { type: tier } });
+ }
+ });
+
+ it("does not inject serviceTier when value is invalid", async () => {
+ const provider = await registerWithConfig(undefined);
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ CONVERSE_MODEL_DESCRIPTOR,
+ runtimePluginConfig(undefined),
+ { serviceTier: "not-a-tier" },
+ );
+ expect(result).not.toHaveProperty("_capturedPayload");
+ });
+
+ it("does not overwrite caller-provided serviceTier in payload", async () => {
+ const provider = await registerWithConfig(undefined);
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ CONVERSE_MODEL_DESCRIPTOR,
+ runtimePluginConfig(undefined),
+ { serviceTier: "flex" },
+ { serviceTier: { type: "priority" } },
+ );
+ expect(result._capturedPayload).toMatchObject({ serviceTier: { type: "priority" } });
+ });
+
+ it("skips injection for non-converse API models", async () => {
+ const provider = await registerWithConfig(undefined);
+ const result = await callWrappedStream(
+ provider,
+ NON_ANTHROPIC_MODEL,
+ { api: "openai-completions", provider: "amazon-bedrock", id: NON_ANTHROPIC_MODEL } as never,
+ runtimePluginConfig(undefined),
+ { serviceTier: "flex" },
+ );
+ expect(result).not.toHaveProperty("_capturedPayload");
+ });
+ });
+
describe("application inference profile cache point injection", () => {
/**
* Invoke wrapStreamFn with a payload containing system/messages, then
diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts
index abef50b3578..01740101e44 100644
--- a/extensions/amazon-bedrock/register.sync.runtime.ts
+++ b/extensions/amazon-bedrock/register.sync.runtime.ts
@@ -34,6 +34,43 @@ type AmazonBedrockPluginConfig = {
guardrail?: GuardrailConfig;
};
+const BEDROCK_SERVICE_TIER_VALUES = ["flex", "priority", "default", "reserved"] as const;
+type BedrockServiceTier = (typeof BEDROCK_SERVICE_TIER_VALUES)[number];
+
+function isBedrockServiceTier(value: string): value is BedrockServiceTier {
+ return BEDROCK_SERVICE_TIER_VALUES.some((tier) => tier === value);
+}
+
+function resolveBedrockServiceTier(
+ extraParams: Record | undefined,
+ warn: (message: string) => void,
+): BedrockServiceTier | undefined {
+ const raw = extraParams?.serviceTier ?? extraParams?.service_tier;
+ if (typeof raw !== "string") {
+ return undefined;
+ }
+ const normalized = raw.trim().toLowerCase();
+ if (isBedrockServiceTier(normalized)) {
+ return normalized;
+ }
+ warn(`ignoring invalid Bedrock service_tier param: ${raw}`);
+ return undefined;
+}
+
+function createBedrockServiceTierWrapper(
+ underlying: StreamFn,
+ serviceTier: BedrockServiceTier,
+): StreamFn {
+ return (model, context, options) => {
+ if (model.api !== "bedrock-converse-stream") {
+ return underlying(model, context, options);
+ }
+ return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
+ payloadObj.serviceTier ??= { type: serviceTier };
+ });
+ };
+}
+
function createGuardrailWrapStreamFn(
innerWrapStreamFn: (ctx: { modelId: string; streamFn?: StreamFn }) => StreamFn | null | undefined,
guardrailConfig: GuardrailConfig,
@@ -484,13 +521,20 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
},
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
...anthropicByModelReplayHooks,
- wrapStreamFn: ({ modelId, config, model, streamFn, thinkingLevel }) => {
+ wrapStreamFn: ({ modelId, config, model, streamFn, thinkingLevel, extraParams }) => {
const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail;
- // Apply cache + guardrail wrapping.
- const wrapped =
- currentGuardrail?.guardrailIdentifier && currentGuardrail?.guardrailVersion
+ let wrapped =
+ (currentGuardrail?.guardrailIdentifier && currentGuardrail?.guardrailVersion
? createGuardrailWrapStreamFn(baseWrapStreamFn, currentGuardrail)({ modelId, streamFn })
- : baseWrapStreamFn({ modelId, streamFn });
+ : baseWrapStreamFn({ modelId, streamFn })) ?? undefined;
+
+ const serviceTier = resolveBedrockServiceTier(extraParams, (message) =>
+ api.logger.warn(message),
+ );
+ if (serviceTier && wrapped) {
+ wrapped = createBedrockServiceTierWrapper(wrapped, serviceTier);
+ }
+
const region = resolveBedrockRegion(config) ?? extractRegionFromBaseUrl(model?.baseUrl);
const mayNeedCacheInjection =
isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId);