mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 12:10:44 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user