Skip to content

Commit e84b866

Browse files
authored
feat(bedrock): adding Amazon Nova (RooCodeInc#2406)
* amazon nova * changeset
1 parent 5b6eae2 commit e84b866

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

.changeset/afraid-items-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
feat(bedrock): adding Amazon Nova

src/api/providers/bedrock.ts

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels
77
import { calculateApiCostOpenAI } from "../../utils/cost"
88
import { ApiStream } from "../transform/stream"
99
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
10-
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
10+
import {
11+
BedrockRuntimeClient,
12+
ConversationRole,
13+
ConverseStreamCommand,
14+
InvokeModelWithResponseStreamCommand,
15+
} from "@aws-sdk/client-bedrock-runtime"
1116

1217
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
1318
export class AwsBedrockHandler implements ApiHandler {
@@ -23,6 +28,12 @@ export class AwsBedrockHandler implements ApiHandler {
2328
let modelId = await this.getModelId()
2429
const model = this.getModel()
2530

31+
// Check if this is an Amazon Nova model
32+
if (modelId.includes("amazon.nova")) {
33+
yield* this.createNovaMessage(systemPrompt, messages, modelId, model)
34+
return
35+
}
36+
2637
// Check if this is a Deepseek model
2738
if (modelId.includes("deepseek")) {
2839
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
@@ -462,4 +473,192 @@ export class AwsBedrockHandler implements ApiHandler {
462473
// Approximate 4 characters per token
463474
return Math.ceil(text.length / 4)
464475
}
476+
477+
/**
478+
* Creates a message using Amazon Nova models through AWS Bedrock
479+
* Implements support for Nova Micro, Nova Lite, and Nova Pro models
480+
*/
481+
private async *createNovaMessage(
482+
systemPrompt: string,
483+
messages: Anthropic.Messages.MessageParam[],
484+
modelId: string,
485+
model: { id: BedrockModelId; info: ModelInfo },
486+
): ApiStream {
487+
// Get Bedrock client with proper credentials
488+
const client = await this.getBedrockClient()
489+
490+
// Format messages for Nova model
491+
const formattedMessages = this.formatNovaMessages(messages)
492+
493+
// Prepare request for Nova model
494+
const command = new ConverseStreamCommand({
495+
modelId: modelId,
496+
messages: formattedMessages,
497+
system: systemPrompt ? [{ text: systemPrompt }] : undefined,
498+
inferenceConfig: {
499+
maxTokens: model.info.maxTokens || 5000,
500+
temperature: 0,
501+
// topP: 0.9, // Alternative: use topP instead of temperature
502+
},
503+
})
504+
505+
// Execute the streaming request and handle response
506+
try {
507+
const response = await client.send(command)
508+
509+
if (response.stream) {
510+
let hasReportedInputTokens = false
511+
512+
for await (const chunk of response.stream) {
513+
// Handle metadata events with token usage information
514+
if (chunk.metadata?.usage) {
515+
// Report complete token usage from the model itself
516+
const inputTokens = chunk.metadata.usage.inputTokens || 0
517+
const outputTokens = chunk.metadata.usage.outputTokens || 0
518+
yield {
519+
type: "usage",
520+
inputTokens,
521+
outputTokens,
522+
totalCost: calculateApiCostOpenAI(model.info, inputTokens, outputTokens, 0, 0),
523+
}
524+
hasReportedInputTokens = true
525+
}
526+
527+
// Handle content delta (text generation)
528+
if (chunk.contentBlockDelta?.delta?.text) {
529+
yield {
530+
type: "text",
531+
text: chunk.contentBlockDelta.delta.text,
532+
}
533+
}
534+
535+
// Handle reasoning content if present
536+
if (chunk.contentBlockDelta?.delta?.reasoningContent?.text) {
537+
yield {
538+
type: "reasoning",
539+
reasoning: chunk.contentBlockDelta.delta.reasoningContent.text,
540+
}
541+
}
542+
543+
// Handle errors
544+
if (chunk.internalServerException) {
545+
yield {
546+
type: "text",
547+
text: `[ERROR] Internal server error: ${chunk.internalServerException.message}`,
548+
}
549+
} else if (chunk.modelStreamErrorException) {
550+
yield {
551+
type: "text",
552+
text: `[ERROR] Model stream error: ${chunk.modelStreamErrorException.message}`,
553+
}
554+
} else if (chunk.validationException) {
555+
yield {
556+
type: "text",
557+
text: `[ERROR] Validation error: ${chunk.validationException.message}`,
558+
}
559+
} else if (chunk.throttlingException) {
560+
yield {
561+
type: "text",
562+
text: `[ERROR] Throttling error: ${chunk.throttlingException.message}`,
563+
}
564+
} else if (chunk.serviceUnavailableException) {
565+
yield {
566+
type: "text",
567+
text: `[ERROR] Service unavailable: ${chunk.serviceUnavailableException.message}`,
568+
}
569+
}
570+
}
571+
}
572+
} catch (error) {
573+
console.error("Error processing Nova model response:", error)
574+
yield {
575+
type: "text",
576+
text: `[ERROR] Failed to process Nova response: ${error instanceof Error ? error.message : String(error)}`,
577+
}
578+
}
579+
}
580+
581+
/**
582+
* Formats messages for Amazon Nova models according to the SDK specification
583+
*/
584+
private formatNovaMessages(messages: Anthropic.Messages.MessageParam[]): { role: ConversationRole; content: any[] }[] {
585+
return messages.map((message) => {
586+
// Determine role (user or assistant)
587+
const role = message.role === "user" ? ConversationRole.USER : ConversationRole.ASSISTANT
588+
589+
// Process content based on type
590+
let content: any[] = []
591+
592+
if (typeof message.content === "string") {
593+
// Simple text content
594+
content = [{ text: message.content }]
595+
} else if (Array.isArray(message.content)) {
596+
// Convert Anthropic content format to Nova content format
597+
content = message.content
598+
.map((item) => {
599+
// Text content
600+
if (item.type === "text") {
601+
return { text: item.text }
602+
}
603+
604+
// Image content
605+
if (item.type === "image") {
606+
// Handle different image source formats
607+
let imageData: Uint8Array
608+
let format = "jpeg" // default format
609+
610+
// Extract format from media_type if available
611+
if (item.source.media_type) {
612+
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
613+
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
614+
if (formatMatch && formatMatch[1]) {
615+
format = formatMatch[1]
616+
// Ensure format is one of the allowed values
617+
if (!["png", "jpeg", "gif", "webp"].includes(format)) {
618+
format = "jpeg" // Default to jpeg if not supported
619+
}
620+
}
621+
}
622+
623+
// Get image data
624+
try {
625+
if (typeof item.source.data === "string") {
626+
// Handle base64 encoded data
627+
const base64Data = item.source.data.replace(/^data:image\/\w+;base64,/, "")
628+
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
629+
} else if (item.source.data && typeof item.source.data === "object") {
630+
// Try to convert to Uint8Array
631+
imageData = new Uint8Array(Buffer.from(item.source.data as any))
632+
} else {
633+
console.error("Unsupported image data format")
634+
return null // Skip this item if format is not supported
635+
}
636+
} catch (error) {
637+
console.error("Could not convert image data to Uint8Array:", error)
638+
return null // Skip this item if conversion fails
639+
}
640+
641+
return {
642+
image: {
643+
format,
644+
source: {
645+
bytes: imageData,
646+
},
647+
},
648+
}
649+
}
650+
651+
// Return null for unsupported content types
652+
return null
653+
})
654+
.filter(Boolean) // Remove any null items
655+
}
656+
657+
// Return formatted message
658+
return {
659+
role,
660+
content,
661+
}
662+
})
663+
}
465664
}

src/shared/api.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,33 @@ export const anthropicModels = {
159159
export type BedrockModelId = keyof typeof bedrockModels
160160
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
161161
export const bedrockModels = {
162+
"amazon.nova-pro-v1:0": {
163+
maxTokens: 5000,
164+
contextWindow: 300_000,
165+
supportsImages: true,
166+
supportsComputerUse: false,
167+
supportsPromptCache: false,
168+
inputPrice: 0.8,
169+
outputPrice: 3.2,
170+
},
171+
"amazon.nova-lite-v1:0": {
172+
maxTokens: 5000,
173+
contextWindow: 300_000,
174+
supportsImages: true,
175+
supportsComputerUse: false,
176+
supportsPromptCache: false,
177+
inputPrice: 0.06,
178+
outputPrice: 0.24,
179+
},
180+
"amazon.nova-micro-v1:0": {
181+
maxTokens: 5000,
182+
contextWindow: 128_000,
183+
supportsImages: false,
184+
supportsComputerUse: false,
185+
supportsPromptCache: false,
186+
inputPrice: 0.035,
187+
outputPrice: 0.14,
188+
},
162189
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
163190
maxTokens: 8192,
164191
contextWindow: 200_000,

0 commit comments

Comments
 (0)