Skip to content

Commit 9f3122f

Browse files
roomote[bot]ellipsis-dev[bot]roomotemrubens
authored
feat: add AWS Bedrock service tier support (#9955)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Roo Code <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent 1be8a99 commit 9f3122f

File tree

23 files changed

+515
-2
lines changed

23 files changed

+515
-2
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
230230
awsBedrockEndpointEnabled: z.boolean().optional(),
231231
awsBedrockEndpoint: z.string().optional(),
232232
awsBedrock1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
233+
awsBedrockServiceTier: z.enum(["STANDARD", "FLEX", "PRIORITY"]).optional(), // AWS Bedrock service tier selection
233234
})
234235

235236
const vertexSchema = apiModelIdProviderModelSchema.extend({

packages/types/src/providers/bedrock.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,31 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
577577
"anthropic.claude-haiku-4-5-20251001-v1:0",
578578
"anthropic.claude-opus-4-5-20251101-v1:0",
579579
] as const
580+
581+
// Amazon Bedrock Service Tier types
582+
export type BedrockServiceTier = "STANDARD" | "FLEX" | "PRIORITY"
583+
584+
// Models that support service tiers based on AWS documentation
585+
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
586+
export const BEDROCK_SERVICE_TIER_MODEL_IDS = [
587+
// Amazon Nova models
588+
"amazon.nova-lite-v1:0",
589+
"amazon.nova-2-lite-v1:0",
590+
"amazon.nova-pro-v1:0",
591+
"amazon.nova-pro-latency-optimized-v1:0",
592+
// DeepSeek models
593+
"deepseek.r1-v1:0",
594+
// Qwen models
595+
"qwen.qwen3-next-80b-a3b",
596+
"qwen.qwen3-coder-480b-a35b-v1:0",
597+
// OpenAI GPT-OSS models
598+
"openai.gpt-oss-20b-1:0",
599+
"openai.gpt-oss-120b-1:0",
600+
] as const
601+
602+
// Service tier pricing multipliers
603+
export const BEDROCK_SERVICE_TIER_PRICING = {
604+
STANDARD: 1.0, // Base price
605+
FLEX: 0.5, // 50% discount from standard
606+
PRIORITY: 1.75, // 75% premium over standard
607+
} as const

src/api/providers/__tests__/bedrock.spec.ts

Lines changed: 242 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {
2525

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

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

@@ -755,4 +755,245 @@ describe("AwsBedrockHandler", () => {
755755
expect(commandArg.modelId).toBe(`us.${BEDROCK_1M_CONTEXT_MODEL_IDS[0]}`)
756756
})
757757
})
758+
759+
describe("service tier feature", () => {
760+
const supportedModelId = BEDROCK_SERVICE_TIER_MODEL_IDS[0] // amazon.nova-lite-v1:0
761+
762+
beforeEach(() => {
763+
mockConverseStreamCommand.mockReset()
764+
})
765+
766+
describe("pricing multipliers in getModel()", () => {
767+
it("should apply FLEX tier pricing with 50% discount", () => {
768+
const handler = new AwsBedrockHandler({
769+
apiModelId: supportedModelId,
770+
awsAccessKey: "test",
771+
awsSecretKey: "test",
772+
awsRegion: "us-east-1",
773+
awsBedrockServiceTier: "FLEX",
774+
})
775+
776+
const model = handler.getModel()
777+
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
778+
inputPrice: number
779+
outputPrice: number
780+
}
781+
782+
// FLEX tier should apply 0.5 multiplier (50% discount)
783+
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 0.5)
784+
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 0.5)
785+
})
786+
787+
it("should apply PRIORITY tier pricing with 75% premium", () => {
788+
const handler = new AwsBedrockHandler({
789+
apiModelId: supportedModelId,
790+
awsAccessKey: "test",
791+
awsSecretKey: "test",
792+
awsRegion: "us-east-1",
793+
awsBedrockServiceTier: "PRIORITY",
794+
})
795+
796+
const model = handler.getModel()
797+
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
798+
inputPrice: number
799+
outputPrice: number
800+
}
801+
802+
// PRIORITY tier should apply 1.75 multiplier (75% premium)
803+
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 1.75)
804+
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 1.75)
805+
})
806+
807+
it("should not modify pricing for STANDARD tier", () => {
808+
const handler = new AwsBedrockHandler({
809+
apiModelId: supportedModelId,
810+
awsAccessKey: "test",
811+
awsSecretKey: "test",
812+
awsRegion: "us-east-1",
813+
awsBedrockServiceTier: "STANDARD",
814+
})
815+
816+
const model = handler.getModel()
817+
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
818+
inputPrice: number
819+
outputPrice: number
820+
}
821+
822+
// STANDARD tier should not modify pricing (1.0 multiplier)
823+
expect(model.info.inputPrice).toBe(baseModel.inputPrice)
824+
expect(model.info.outputPrice).toBe(baseModel.outputPrice)
825+
})
826+
827+
it("should not apply service tier pricing for unsupported models", () => {
828+
const unsupportedModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
829+
const handler = new AwsBedrockHandler({
830+
apiModelId: unsupportedModelId,
831+
awsAccessKey: "test",
832+
awsSecretKey: "test",
833+
awsRegion: "us-east-1",
834+
awsBedrockServiceTier: "FLEX", // Try to apply FLEX tier
835+
})
836+
837+
const model = handler.getModel()
838+
const baseModel = bedrockModels[unsupportedModelId as keyof typeof bedrockModels] as {
839+
inputPrice: number
840+
outputPrice: number
841+
}
842+
843+
// Pricing should remain unchanged for unsupported models
844+
expect(model.info.inputPrice).toBe(baseModel.inputPrice)
845+
expect(model.info.outputPrice).toBe(baseModel.outputPrice)
846+
})
847+
})
848+
849+
describe("service_tier parameter in API requests", () => {
850+
it("should include service_tier as top-level parameter for supported models", async () => {
851+
const handler = new AwsBedrockHandler({
852+
apiModelId: supportedModelId,
853+
awsAccessKey: "test",
854+
awsSecretKey: "test",
855+
awsRegion: "us-east-1",
856+
awsBedrockServiceTier: "PRIORITY",
857+
})
858+
859+
const messages: Anthropic.Messages.MessageParam[] = [
860+
{
861+
role: "user",
862+
content: "Test message",
863+
},
864+
]
865+
866+
const generator = handler.createMessage("", messages)
867+
await generator.next() // Start the generator
868+
869+
// Verify the command was created with service_tier at top level
870+
// Per AWS documentation, service_tier must be a top-level parameter, not inside additionalModelRequestFields
871+
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
872+
expect(mockConverseStreamCommand).toHaveBeenCalled()
873+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
874+
875+
// service_tier should be at the top level of the payload
876+
expect(commandArg.service_tier).toBe("PRIORITY")
877+
// service_tier should NOT be in additionalModelRequestFields
878+
if (commandArg.additionalModelRequestFields) {
879+
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
880+
}
881+
})
882+
883+
it("should include service_tier FLEX as top-level parameter", async () => {
884+
const handler = new AwsBedrockHandler({
885+
apiModelId: supportedModelId,
886+
awsAccessKey: "test",
887+
awsSecretKey: "test",
888+
awsRegion: "us-east-1",
889+
awsBedrockServiceTier: "FLEX",
890+
})
891+
892+
const messages: Anthropic.Messages.MessageParam[] = [
893+
{
894+
role: "user",
895+
content: "Test message",
896+
},
897+
]
898+
899+
const generator = handler.createMessage("", messages)
900+
await generator.next() // Start the generator
901+
902+
expect(mockConverseStreamCommand).toHaveBeenCalled()
903+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
904+
905+
// service_tier should be at the top level of the payload
906+
expect(commandArg.service_tier).toBe("FLEX")
907+
// service_tier should NOT be in additionalModelRequestFields
908+
if (commandArg.additionalModelRequestFields) {
909+
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
910+
}
911+
})
912+
913+
it("should NOT include service_tier for unsupported models", async () => {
914+
const unsupportedModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
915+
const handler = new AwsBedrockHandler({
916+
apiModelId: unsupportedModelId,
917+
awsAccessKey: "test",
918+
awsSecretKey: "test",
919+
awsRegion: "us-east-1",
920+
awsBedrockServiceTier: "PRIORITY", // Try to apply PRIORITY tier
921+
})
922+
923+
const messages: Anthropic.Messages.MessageParam[] = [
924+
{
925+
role: "user",
926+
content: "Test message",
927+
},
928+
]
929+
930+
const generator = handler.createMessage("", messages)
931+
await generator.next() // Start the generator
932+
933+
expect(mockConverseStreamCommand).toHaveBeenCalled()
934+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
935+
936+
// Service tier should NOT be included for unsupported models (at top level or in additionalModelRequestFields)
937+
expect(commandArg.service_tier).toBeUndefined()
938+
if (commandArg.additionalModelRequestFields) {
939+
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
940+
}
941+
})
942+
943+
it("should NOT include service_tier when not specified", async () => {
944+
const handler = new AwsBedrockHandler({
945+
apiModelId: supportedModelId,
946+
awsAccessKey: "test",
947+
awsSecretKey: "test",
948+
awsRegion: "us-east-1",
949+
// No awsBedrockServiceTier specified
950+
})
951+
952+
const messages: Anthropic.Messages.MessageParam[] = [
953+
{
954+
role: "user",
955+
content: "Test message",
956+
},
957+
]
958+
959+
const generator = handler.createMessage("", messages)
960+
await generator.next() // Start the generator
961+
962+
expect(mockConverseStreamCommand).toHaveBeenCalled()
963+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
964+
965+
// Service tier should NOT be included when not specified (at top level or in additionalModelRequestFields)
966+
expect(commandArg.service_tier).toBeUndefined()
967+
if (commandArg.additionalModelRequestFields) {
968+
expect(commandArg.additionalModelRequestFields.service_tier).toBeUndefined()
969+
}
970+
})
971+
})
972+
973+
describe("service tier with cross-region inference", () => {
974+
it("should apply service tier pricing with cross-region inference prefix", () => {
975+
const handler = new AwsBedrockHandler({
976+
apiModelId: supportedModelId,
977+
awsAccessKey: "test",
978+
awsSecretKey: "test",
979+
awsRegion: "us-east-1",
980+
awsUseCrossRegionInference: true,
981+
awsBedrockServiceTier: "FLEX",
982+
})
983+
984+
const model = handler.getModel()
985+
const baseModel = bedrockModels[supportedModelId as keyof typeof bedrockModels] as {
986+
inputPrice: number
987+
outputPrice: number
988+
}
989+
990+
// Model ID should have cross-region prefix
991+
expect(model.id).toBe(`us.${supportedModelId}`)
992+
993+
// FLEX tier pricing should still be applied
994+
expect(model.info.inputPrice).toBe(baseModel.inputPrice * 0.5)
995+
expect(model.info.outputPrice).toBe(baseModel.outputPrice * 0.5)
996+
})
997+
})
998+
})
758999
})

src/api/providers/bedrock.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type ModelInfo,
1919
type ProviderSettings,
2020
type BedrockModelId,
21+
type BedrockServiceTier,
2122
bedrockDefaultModelId,
2223
bedrockModels,
2324
bedrockDefaultPromptRouterModelId,
@@ -27,6 +28,8 @@ import {
2728
AWS_INFERENCE_PROFILE_MAPPING,
2829
BEDROCK_1M_CONTEXT_MODEL_IDS,
2930
BEDROCK_GLOBAL_INFERENCE_MODEL_IDS,
31+
BEDROCK_SERVICE_TIER_MODEL_IDS,
32+
BEDROCK_SERVICE_TIER_PRICING,
3033
} from "@roo-code/types"
3134

3235
import { ApiStream } from "../transform/stream"
@@ -74,6 +77,13 @@ interface BedrockPayload {
7477
toolConfig?: ToolConfiguration
7578
}
7679

80+
// Extended payload type that includes service_tier as a top-level parameter
81+
// AWS Bedrock service tiers (STANDARD, FLEX, PRIORITY) are specified at the top level
82+
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
83+
type BedrockPayloadWithServiceTier = BedrockPayload & {
84+
service_tier?: BedrockServiceTier
85+
}
86+
7787
// Define specific types for content block events to avoid 'as any' usage
7888
// These handle the multiple possible structures returned by AWS SDK
7989
interface ContentBlockStartEvent {
@@ -433,6 +443,17 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
433443
additionalModelRequestFields.anthropic_beta = anthropicBetas
434444
}
435445

446+
// Determine if service tier should be applied (checked later when building payload)
447+
const useServiceTier =
448+
this.options.awsBedrockServiceTier && BEDROCK_SERVICE_TIER_MODEL_IDS.includes(baseModelId as any)
449+
if (useServiceTier) {
450+
logger.info("Service tier specified for Bedrock request", {
451+
ctx: "bedrock",
452+
modelId: modelConfig.id,
453+
serviceTier: this.options.awsBedrockServiceTier,
454+
})
455+
}
456+
436457
// Build tool configuration if native tools are enabled
437458
let toolConfig: ToolConfiguration | undefined
438459
if (useNativeTools && metadata?.tools) {
@@ -442,7 +463,10 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
442463
}
443464
}
444465

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

456482
// Create AbortController with 10 minute timeout
@@ -1089,6 +1115,30 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
10891115
defaultTemperature: BEDROCK_DEFAULT_TEMPERATURE,
10901116
})
10911117

1118+
// Apply service tier pricing if specified and model supports it
1119+
const baseModelIdForTier = this.parseBaseModelId(modelConfig.id)
1120+
if (this.options.awsBedrockServiceTier && BEDROCK_SERVICE_TIER_MODEL_IDS.includes(baseModelIdForTier as any)) {
1121+
const pricingMultiplier = BEDROCK_SERVICE_TIER_PRICING[this.options.awsBedrockServiceTier]
1122+
if (pricingMultiplier && pricingMultiplier !== 1.0) {
1123+
// Apply pricing multiplier to all price fields
1124+
modelConfig.info = {
1125+
...modelConfig.info,
1126+
inputPrice: modelConfig.info.inputPrice
1127+
? modelConfig.info.inputPrice * pricingMultiplier
1128+
: undefined,
1129+
outputPrice: modelConfig.info.outputPrice
1130+
? modelConfig.info.outputPrice * pricingMultiplier
1131+
: undefined,
1132+
cacheWritesPrice: modelConfig.info.cacheWritesPrice
1133+
? modelConfig.info.cacheWritesPrice * pricingMultiplier
1134+
: undefined,
1135+
cacheReadsPrice: modelConfig.info.cacheReadsPrice
1136+
? modelConfig.info.cacheReadsPrice * pricingMultiplier
1137+
: undefined,
1138+
}
1139+
}
1140+
}
1141+
10921142
// Don't override maxTokens/contextWindow here; handled in getModelById (and includes user overrides)
10931143
return { ...modelConfig, ...params } as {
10941144
id: BedrockModelId | string

0 commit comments

Comments
 (0)