Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
awsBedrockEndpointEnabled: z.boolean().optional(),
awsBedrockEndpoint: z.string().optional(),
awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
awsBedrockServiceTier: z.enum(["STANDARD", "FLEX", "PRIORITY"]).optional(), // AWS Bedrock service tier selection
})

const vertexSchema = apiModelIdProviderModelSchema.extend({
Expand Down
28 changes: 28 additions & 0 deletions packages/types/src/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,31 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-opus-4-5-20251101-v1:0",
] as const

// Amazon Bedrock Service Tier types
export type BedrockServiceTier = "STANDARD" | "FLEX" | "PRIORITY"

// Models that support service tiers based on AWS documentation
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
export const BEDROCK_SERVICE_TIER_MODEL_IDS = [
// Amazon Nova models
"amazon.nova-lite-v1:0",
"amazon.nova-2-lite-v1:0",
"amazon.nova-pro-v1:0",
"amazon.nova-pro-latency-optimized-v1:0",
// DeepSeek models
"deepseek.r1-v1:0",
// Qwen models
"qwen.qwen3-next-80b-a3b",
"qwen.qwen3-coder-480b-a35b-v1:0",
// OpenAI GPT-OSS models
"openai.gpt-oss-20b-1:0",
"openai.gpt-oss-120b-1:0",
] as const

// Service tier pricing multipliers
export const BEDROCK_SERVICE_TIER_PRICING = {
STANDARD: 1.0, // Base price
FLEX: 0.5, // 50% discount from standard
PRIORITY: 1.75, // 75% premium over standard
} as const
243 changes: 242 additions & 1 deletion src/api/providers/__tests__/bedrock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {

import { AwsBedrockHandler } from "../bedrock"
import { ConverseStreamCommand, BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
import { BEDROCK_1M_CONTEXT_MODEL_IDS } from "@roo-code/types"
import { BEDROCK_1M_CONTEXT_MODEL_IDS, BEDROCK_SERVICE_TIER_MODEL_IDS, bedrockModels } from "@roo-code/types"

import type { Anthropic } from "@anthropic-ai/sdk"

Expand Down Expand Up @@ -755,4 +755,245 @@ describe("AwsBedrockHandler", () => {
expect(commandArg.modelId).toBe(`us.${BEDROCK_1M_CONTEXT_MODEL_IDS[0]}`)
})
})

describe("service tier feature", () => {
const supportedModelId = BEDROCK_SERVICE_TIER_MODEL_IDS[0] // amazon.nova-lite-v1:0

beforeEach(() => {
mockConverseStreamCommand.mockReset()
})

describe("pricing multipliers in getModel()", () => {
it("should apply FLEX tier pricing with 50% discount", () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "FLEX",
})

const model = handler.getModel()
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
inputPrice: number
outputPrice: number
}

// FLEX tier should apply 0.5 multiplier (50% discount)
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 0.5)
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 0.5)
})

it("should apply PRIORITY tier pricing with 75% premium", () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "PRIORITY",
})

const model = handler.getModel()
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
inputPrice: number
outputPrice: number
}

// PRIORITY tier should apply 1.75 multiplier (75% premium)
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 1.75)
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 1.75)
})

it("should not modify pricing for STANDARD tier", () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "STANDARD",
})

const model = handler.getModel()
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
inputPrice: number
outputPrice: number
}

// STANDARD tier should not modify pricing (1.0 multiplier)
expect(model.info.inputPrice).toBe(baseModel.inputPrice)
expect(model.info.outputPrice).toBe(baseModel.outputPrice)
})

it("should not apply service tier pricing for unsupported models", () => {
const unsupportedModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
const handler = new AwsBedrockHandler({
apiModelId: unsupportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "FLEX", // Try to apply FLEX tier
})

const model = handler.getModel()
const baseModel = bedrockModels[unsupportedModelId as keyof typeof bedrockModels] as {
inputPrice: number
outputPrice: number
}

// Pricing should remain unchanged for unsupported models
expect(model.info.inputPrice).toBe(baseModel.inputPrice)
expect(model.info.outputPrice).toBe(baseModel.outputPrice)
})
})

describe("service_tier parameter in API requests", () => {
it("should include service_tier as top-level parameter for supported models", async () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "PRIORITY",
})

const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Test message",
},
]

const generator = handler.createMessage("", messages)
await generator.next() // Start the generator

// Verify the command was created with service_tier at top level
// Per AWS documentation, service_tier must be a top-level parameter, not inside additionalModelRequestFields
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any

// service_tier should be at the top level of the payload
expect(commandArg.service_tier).toBe("PRIORITY")
// service_tier should NOT be in additionalModelRequestFields
if (commandArg.additionalModelRequestFields) {
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
}
})

it("should include service_tier FLEX as top-level parameter", async () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "FLEX",
})

const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Test message",
},
]

const generator = handler.createMessage("", messages)
await generator.next() // Start the generator

expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any

// service_tier should be at the top level of the payload
expect(commandArg.service_tier).toBe("FLEX")
// service_tier should NOT be in additionalModelRequestFields
if (commandArg.additionalModelRequestFields) {
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
}
})

it("should NOT include service_tier for unsupported models", async () => {
const unsupportedModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
const handler = new AwsBedrockHandler({
apiModelId: unsupportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrockServiceTier: "PRIORITY", // Try to apply PRIORITY tier
})

const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Test message",
},
]

const generator = handler.createMessage("", messages)
await generator.next() // Start the generator

expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any

// Service tier should NOT be included for unsupported models (at top level or in additionalModelRequestFields)
expect(commandArg.service_tier).toBeUndefined()
if (commandArg.additionalModelRequestFields) {
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
}
})

it("should NOT include service_tier when not specified", async () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
// No awsBedrockServiceTier specified
})

const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Test message",
},
]

const generator = handler.createMessage("", messages)
await generator.next() // Start the generator

expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any

// Service tier should NOT be included when not specified (at top level or in additionalModelRequestFields)
expect(commandArg.service_tier).toBeUndefined()
if (commandArg.additionalModelRequestFields) {
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
}
})
})

describe("service tier with cross-region inference", () => {
it("should apply service tier pricing with cross-region inference prefix", () => {
const handler = new AwsBedrockHandler({
apiModelId: supportedModelId,
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsUseCrossRegionInference: true,
awsBedrockServiceTier: "FLEX",
})

const model = handler.getModel()
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
inputPrice: number
outputPrice: number
}

// Model ID should have cross-region prefix
expect(model.id).toBe(`us.${supportedModelId}`)

// FLEX tier pricing should still be applied
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 0.5)
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 0.5)
})
})
})
})
52 changes: 51 additions & 1 deletion src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type ModelInfo,
type ProviderSettings,
type BedrockModelId,
type BedrockServiceTier,
bedrockDefaultModelId,
bedrockModels,
bedrockDefaultPromptRouterModelId,
Expand All @@ -27,6 +28,8 @@ import {
AWS_INFERENCE_PROFILE_MAPPING,
BEDROCK_1M_CONTEXT_MODEL_IDS,
BEDROCK_GLOBAL_INFERENCE_MODEL_IDS,
BEDROCK_SERVICE_TIER_MODEL_IDS,
BEDROCK_SERVICE_TIER_PRICING,
} from "@roo-code/types"

import { ApiStream } from "../transform/stream"
Expand Down Expand Up @@ -74,6 +77,13 @@ interface BedrockPayload {
toolConfig?: ToolConfiguration
}

// Extended payload type that includes service_tier as a top-level parameter
// AWS Bedrock service tiers (STANDARD, FLEX, PRIORITY) are specified at the top level
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
type BedrockPayloadWithServiceTier = BedrockPayload & {
service_tier?: BedrockServiceTier
}

// Define specific types for content block events to avoid 'as any' usage
// These handle the multiple possible structures returned by AWS SDK
interface ContentBlockStartEvent {
Expand Down Expand Up @@ -433,6 +443,17 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
additionalModelRequestFields.anthropic_beta = anthropicBetas
}

// Determine if service tier should be applied (checked later when building payload)
const useServiceTier =
this.options.awsBedrockServiceTier && BEDROCK_SERVICE_TIER_MODEL_IDS.includes(baseModelId as any)
if (useServiceTier) {
logger.info("Service tier specified for Bedrock request", {
ctx: "bedrock",
modelId: modelConfig.id,
serviceTier: this.options.awsBedrockServiceTier,
})
}

// Build tool configuration if native tools are enabled
let toolConfig: ToolConfiguration | undefined
if (useNativeTools && metadata?.tools) {
Expand All @@ -442,7 +463,10 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
}

const payload: BedrockPayload = {
// Build payload with optional service_tier at top level
// Service tier is a top-level parameter per AWS documentation, NOT inside additionalModelRequestFields
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
const payload: BedrockPayloadWithServiceTier = {
modelId: modelConfig.id,
messages: formatted.messages,
system: formatted.system,
Expand All @@ -451,6 +475,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
// Add anthropic_version at top level when using thinking features
...(thinkingEnabled && { anthropic_version: "bedrock-2023-05-31" }),
...(toolConfig && { toolConfig }),
// Add service_tier as a top-level parameter (not inside additionalModelRequestFields)
...(useServiceTier && { service_tier: this.options.awsBedrockServiceTier }),
}

// Create AbortController with 10 minute timeout
Expand Down Expand Up @@ -1089,6 +1115,30 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
defaultTemperature: BEDROCK_DEFAULT_TEMPERATURE,
})

// Apply service tier pricing if specified and model supports it
const baseModelIdForTier = this.parseBaseModelId(modelConfig.id)
if (this.options.awsBedrockServiceTier && BEDROCK_SERVICE_TIER_MODEL_IDS.includes(baseModelIdForTier as any)) {
const pricingMultiplier = BEDROCK_SERVICE_TIER_PRICING[this.options.awsBedrockServiceTier]
if (pricingMultiplier && pricingMultiplier !== 1.0) {
// Apply pricing multiplier to all price fields
modelConfig.info = {
...modelConfig.info,
inputPrice: modelConfig.info.inputPrice
? modelConfig.info.inputPrice * pricingMultiplier
: undefined,
outputPrice: modelConfig.info.outputPrice
? modelConfig.info.outputPrice * pricingMultiplier
: undefined,
cacheWritesPrice: modelConfig.info.cacheWritesPrice
? modelConfig.info.cacheWritesPrice * pricingMultiplier
: undefined,
cacheReadsPrice: modelConfig.info.cacheReadsPrice
? modelConfig.info.cacheReadsPrice * pricingMultiplier
: undefined,
}
}
}

// Don't override maxTokens/contextWindow here; handled in getModelById (and includes user overrides)
return { ...modelConfig, ...params } as {
id: BedrockModelId | string
Expand Down
Loading
Loading