feat(bedrock): add service_tier parameter support

- Add resolveBedrockServiceTier() and createBedrockServiceTierWrapper()
  to bedrock-stream-wrappers.ts
- Export service tier functions from provider-stream-shared.ts SDK barrel
- Wire service tier into Bedrock provider wrapStreamFn
- Accepts serviceTier or service_tier via agents.defaults.params

Valid values: default, flex, priority, reserved

Authored by Deepseek-v4-Pro, reviewed by rob@mobilinkd.com.
This commit is contained in:
Rob Riggs
2026-04-25 19:39:58 -07:00
committed by Ayaan Zaidi
parent a3e48fd259
commit 0ef1f36286
4 changed files with 179 additions and 14 deletions

View File

@@ -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.

View File

@@ -256,6 +256,49 @@ openclaw models list
</Accordion>
<Accordion title="Service tier">
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["<model-key>"].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.
</Accordion>
<Accordion title="Claude Opus 4.7 temperature">
Bedrock rejects the `temperature` parameter for Claude Opus 4.7. OpenClaw
omits `temperature` automatically for any Opus 4.7 Bedrock ref, including

View File

@@ -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<string, unknown>,
payload: Record<string, unknown> = {},
): Promise<Record<string, unknown>> {
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<string, unknown> = {};
await (result.onPayload as (p: Record<string, unknown>, model: unknown) => Promise<unknown>)(
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

View File

@@ -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<string, unknown> | 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);