Skip to content

Commit de3371e

Browse files
committed
feat(amazon-bedrock): add Amazon Bedrock client
1 parent 5dca254 commit de3371e

File tree

14 files changed

+361
-3
lines changed

14 files changed

+361
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- Support Amazon Bedrock client.
8+
59
## [2.12.0] - 2025-06-05
610

711
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ plugin and configure a LLM API client in plugin's settings: <kbd>Settings</kbd>
3737

3838
## Supported models
3939

40+
- Amazon Bedrock
4041
- Anthropic
4142
- Azure Open AI
4243
- Gemini Google AI

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ dependencies {
111111
implementation("dev.langchain4j:langchain4j-google-ai-gemini")
112112
implementation("dev.langchain4j:langchain4j-github-models")
113113
implementation("dev.langchain4j:langchain4j-mistral-ai")
114+
implementation("dev.langchain4j:langchain4j-bedrock")
114115

115116
implementation(platform("dev.langchain4j:langchain4j-community-bom:1.0.1-beta6"))
116117
// The Baidu Qianfan Large Model Platform, including the ERNIE series, can be accessed at https://docs.langchain4j.dev/integrations/language-models/qianfan/.

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/Icons.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object Icons {
2828
val HUGGING_FACE = AICommitsIcon("/icons/huggingface.svg", null)
2929
val GITHUB = AICommitsIcon("/icons/github15bright.svg", "/icons/github15dark.svg")
3030
val MISTRAL = AICommitsIcon("/icons/mistral.svg", null)
31+
val AMAZON_BEDROCK = AICommitsIcon("/icons/amazonBedrock15.svg", "/icons/amazonBedrock15.svg")
3132

3233
object Process {
3334
val STOP = AICommitsIcon("/icons/stop.svg", "/icons/stop_dark.svg")

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings2.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.getCredentialA
55
import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification
66
import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification
77
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientConfiguration
8+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock.AmazonBedrockClientConfiguration
89
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.AnthropicClientConfiguration
910
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration
1011
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration
@@ -65,7 +66,8 @@ class AppSettings2 : PersistentStateComponent<AppSettings2> {
6566
AzureOpenAiClientConfiguration::class,
6667
HuggingFaceClientConfiguration::class,
6768
GitHubModelsClientConfiguration::class,
68-
MistralAIClientConfiguration::class
69+
MistralAIClientConfiguration::class,
70+
AmazonBedrockClientConfiguration::class
6971
],
7072
style = XCollection.Style.v2
7173
)

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientPanel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ abstract class LLMClientPanel(
6262
}
6363
}
6464

65-
open fun Panel.modelIdRow(labelKey: String = "settings.llmClient.modelId") {
65+
open fun Panel.modelIdRow(labelKey: String = "settings.llmClient.modelId", commentKey: String? = null) {
6666
row {
6767
label(message(labelKey))
6868
.widthGroup("label")
@@ -79,6 +79,7 @@ abstract class LLMClientPanel(
7979
})
8080
.align(Align.FILL)
8181
.resizableColumn()
82+
.apply { commentKey?.let { comment(message(commentKey)) } }
8283

8384
getRefreshModelsFunction()?.let { f ->
8485
button(message("settings.refreshModels")) {

src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/clients/LLMClientTable.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.blarc.ai.commits.intellij.plugin.settings.clients
33
import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
44
import com.github.blarc.ai.commits.intellij.plugin.createColumn
55
import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings2
6+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock.AmazonBedrockClientConfiguration
67
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.anthropic.AnthropicClientConfiguration
78
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.azureOpenAi.AzureOpenAiClientConfiguration
89
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.geminiGoogle.GeminiGoogleClientConfiguration
@@ -157,7 +158,8 @@ class LLMClientTable {
157158
AzureOpenAiClientConfiguration(),
158159
HuggingFaceClientConfiguration(),
159160
GitHubModelsClientConfiguration(),
160-
MistralAIClientConfiguration()
161+
MistralAIClientConfiguration(),
162+
AmazonBedrockClientConfiguration()
161163
).sortedBy { it.getClientName() }
162164
} else {
163165
listOf(newLLMClientConfiguration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock;
2+
3+
import com.github.blarc.ai.commits.intellij.plugin.Icons
4+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientConfiguration
5+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientSharedState
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.util.xmlb.Converter
8+
import com.intellij.util.xmlb.annotations.Attribute
9+
import com.intellij.util.xmlb.annotations.OptionTag
10+
import com.intellij.util.xmlb.annotations.Transient
11+
import com.intellij.vcs.commit.AbstractCommitWorkflowHandler
12+
import kotlinx.coroutines.Job
13+
import software.amazon.awssdk.regions.Region
14+
import javax.swing.Icon
15+
16+
class AmazonBedrockClientConfiguration : LLMClientConfiguration(
17+
"Amazon Bedrock",
18+
"us.amazon.nova-lite-v1:0",
19+
"0.7"
20+
) {
21+
22+
@Attribute
23+
var accessKeyId: String? = null
24+
@Attribute
25+
var accessKeyIsStored: Boolean = false
26+
@Transient
27+
var accessKey: String? = null
28+
@Attribute
29+
var timeout: Int = 30
30+
@Attribute
31+
var topP: Double? = null
32+
@Attribute
33+
var topK: Int? = null
34+
@Attribute
35+
var maxOutputTokens: Int? = null
36+
@OptionTag(converter = RegionConverter::class)
37+
var region: Region = Region.EU_CENTRAL_1
38+
39+
companion object {
40+
const val CLIENT_NAME = "Amazon Bedrock"
41+
}
42+
43+
override fun getClientName(): String {
44+
return CLIENT_NAME
45+
}
46+
47+
override fun getClientIcon(): Icon {
48+
return Icons.AMAZON_BEDROCK.getThemeBasedIcon()
49+
}
50+
51+
override fun getSharedState(): LLMClientSharedState {
52+
return AmazonBedrockClientSharedState.getInstance()
53+
}
54+
55+
override fun generateCommitMessage(commitWorkflowHandler: AbstractCommitWorkflowHandler<*, *>, project: Project) {
56+
return AmazonBedrockClientService.getInstance().generateCommitMessage(this, commitWorkflowHandler, project)
57+
}
58+
59+
override fun getGenerateCommitMessageJob(): Job? {
60+
return AmazonBedrockClientService.getInstance().generateCommitMessageJob
61+
}
62+
63+
override fun clone(): LLMClientConfiguration {
64+
val copy = AmazonBedrockClientConfiguration()
65+
copy.id = id
66+
copy.name = name
67+
copy.modelId = modelId
68+
copy.temperature = temperature
69+
copy.accessKeyId = accessKeyId
70+
copy.accessKeyIsStored = accessKeyIsStored
71+
copy.accessKey = accessKey
72+
copy.timeout = timeout
73+
copy.topP = topP
74+
copy.topK = topK
75+
copy.maxOutputTokens = maxOutputTokens
76+
copy.region = region
77+
78+
return copy
79+
}
80+
81+
override fun panel() = AmazonBedrockClientPanel(this)
82+
83+
class RegionConverter : Converter<Region>() {
84+
override fun toString(value: Region): String? {
85+
return value.toString()
86+
}
87+
88+
override fun fromString(value: String): Region? {
89+
return Region.regions().find { it.id() == value }
90+
}
91+
}
92+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock;
2+
3+
import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message
4+
import com.github.blarc.ai.commits.intellij.plugin.emptyText
5+
import com.github.blarc.ai.commits.intellij.plugin.isInt
6+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientPanel
7+
import com.intellij.openapi.ui.ComboBox
8+
import com.intellij.ui.components.JBPasswordField
9+
import com.intellij.ui.components.JBTextField
10+
import com.intellij.ui.dsl.builder.*
11+
import software.amazon.awssdk.regions.Region
12+
13+
class AmazonBedrockClientPanel private constructor(
14+
private val clientConfiguration: AmazonBedrockClientConfiguration,
15+
val service: AmazonBedrockClientService
16+
) : LLMClientPanel(clientConfiguration) {
17+
18+
private val accessKeyIdField = JBTextField()
19+
private val accessKeyPasswordField = JBPasswordField()
20+
private val regionComboBox = ComboBox(Region.regions().toTypedArray())
21+
private val maxOutputTokensTextField = JBTextField()
22+
private val topPTextField = JBTextField()
23+
private val topKTextField = JBTextField()
24+
25+
constructor(configuration: AmazonBedrockClientConfiguration) : this(configuration, AmazonBedrockClientService.getInstance())
26+
27+
override fun create() = panel {
28+
nameRow()
29+
modelIdRow(commentKey = "settings.amazonBedrock.modelId.comment")
30+
temperatureRow()
31+
timeoutRow(clientConfiguration::timeout)
32+
accessKeyIdRow()
33+
accessKeyRow()
34+
regionRow()
35+
maxTokens()
36+
topPDoubleRow(topPTextField, clientConfiguration::topP.toNullableProperty())
37+
topKRow(topKTextField, clientConfiguration::topK.toNullableProperty())
38+
verifyRow()
39+
}
40+
41+
private fun Panel.accessKeyIdRow() {
42+
row {
43+
label(message("settings.amazonBedrock.accessKeyId"))
44+
.widthGroup("label")
45+
cell(accessKeyIdField)
46+
.bindText(clientConfiguration::accessKeyId.toNonNullableProperty(""))
47+
.emptyText(message("settings.amazonBedrock.accessKeyId.example"))
48+
.resizableColumn()
49+
.align(Align.FILL)
50+
}
51+
}
52+
53+
private fun Panel.accessKeyRow() {
54+
row {
55+
label(message("settings.amazonBedrock.accessKey"))
56+
.widthGroup("label")
57+
cell(accessKeyPasswordField)
58+
.bindText(getter = { "" }, setter = {
59+
AmazonBedrockClientService.getInstance().saveToken(clientConfiguration, it)
60+
})
61+
.emptyText(if (clientConfiguration.accessKeyIsStored) message("settings.llmClient.token.stored") else message("settings.amazonBedrock.accessKey.example"))
62+
.resizableColumn()
63+
.align(Align.FILL)
64+
// maxLineLength was eye-balled, but prevents the dialog getting wider
65+
.comment(message("settings.amazonBedrock.accessKey.comment"), 50)
66+
}
67+
}
68+
69+
private fun Panel.regionRow() {
70+
row {
71+
label(message("settings.amazonBedrock.region"))
72+
.widthGroup("label")
73+
cell(regionComboBox)
74+
.applyToComponent {
75+
isEditable = true
76+
}
77+
.bindItem(clientConfiguration::region.toNullableProperty())
78+
.align(Align.FILL)
79+
80+
}
81+
}
82+
83+
private fun Panel.maxTokens() {
84+
row {
85+
label(message("settings.amazonBedrock.maxOutputTokens"))
86+
.widthGroup("label")
87+
cell(maxOutputTokensTextField)
88+
.bindText({ clientConfiguration.maxOutputTokens?.toString() ?: "" }, { s -> clientConfiguration::maxOutputTokens.set(s.toInt()) })
89+
.align(Align.FILL)
90+
.validationOnInput { isInt(it.text) }
91+
.resizableColumn()
92+
}
93+
}
94+
95+
override fun verifyConfiguration() {
96+
// Configuration passed to panel is already a copy of the original or a new configuration
97+
clientConfiguration.modelId = modelComboBox.item
98+
clientConfiguration.temperature = temperatureTextField.text
99+
clientConfiguration.accessKeyId = accessKeyIdField.text
100+
clientConfiguration.accessKey = String(accessKeyPasswordField.password)
101+
clientConfiguration.timeout = socketTimeoutTextField.text.toInt()
102+
clientConfiguration.topP = topPTextField.text.toDoubleOrNull()
103+
clientConfiguration.topK = topKTextField.text.toIntOrNull()
104+
clientConfiguration.maxOutputTokens = maxOutputTokensTextField.text.toIntOrNull()
105+
clientConfiguration.region = regionComboBox.selectedItem as Region
106+
service.verifyConfiguration(clientConfiguration, verifyLabel)
107+
}
108+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.github.blarc.ai.commits.intellij.plugin.settings.clients.amazonBedrock;
2+
3+
import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.getCredentialAttributes
4+
import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.retrieveToken
5+
import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification
6+
import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification
7+
import com.github.blarc.ai.commits.intellij.plugin.settings.clients.LLMClientService
8+
import com.intellij.ide.passwordSafe.PasswordSafe
9+
import com.intellij.openapi.components.Service
10+
import com.intellij.openapi.components.service
11+
import com.intellij.util.text.nullize
12+
import dev.langchain4j.model.bedrock.BedrockChatModel
13+
import dev.langchain4j.model.bedrock.BedrockChatRequestParameters
14+
import dev.langchain4j.model.bedrock.BedrockStreamingChatModel
15+
import dev.langchain4j.model.chat.ChatModel
16+
import dev.langchain4j.model.chat.StreamingChatModel
17+
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.launch
20+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
21+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
22+
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient
23+
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient
24+
import java.time.Duration
25+
26+
@Service(Service.Level.APP)
27+
class AmazonBedrockClientService(private val cs: CoroutineScope) : LLMClientService<AmazonBedrockClientConfiguration>(cs) {
28+
29+
companion object {
30+
@JvmStatic
31+
fun getInstance(): AmazonBedrockClientService = service()
32+
}
33+
34+
override suspend fun buildChatModel(client: AmazonBedrockClientConfiguration): ChatModel {
35+
val accessKey = client.accessKeyId.nullize(true) ?: retrieveToken(client.id)?.toString(true)
36+
val credentials = AwsBasicCredentials.builder()
37+
.accessKeyId(client.accessKeyId)
38+
.secretAccessKey(accessKey)
39+
.build()
40+
41+
return BedrockChatModel.builder()
42+
.modelId(client.modelId)
43+
.region(client.region)
44+
.timeout(Duration.ofSeconds(client.timeout.toLong()))
45+
.client(BedrockRuntimeClient.builder()
46+
.region(client.region)
47+
.credentialsProvider(StaticCredentialsProvider.create(credentials)).build())
48+
.defaultRequestParameters(
49+
BedrockChatRequestParameters.builder()
50+
.topP(client.topP)
51+
.topK(client.topK)
52+
.temperature(client.temperature.toDouble())
53+
.maxOutputTokens(client.maxOutputTokens)
54+
.build()
55+
)
56+
.build()
57+
}
58+
59+
override suspend fun buildStreamingChatModel(client: AmazonBedrockClientConfiguration): StreamingChatModel? {
60+
val accessKey = client.accessKeyId.nullize(true) ?: retrieveToken(client.id)?.toString(true)
61+
val credentials = AwsBasicCredentials.builder()
62+
.accessKeyId(client.accessKeyId)
63+
.secretAccessKey(accessKey)
64+
.build()
65+
66+
return BedrockStreamingChatModel.builder()
67+
.modelId(client.modelId)
68+
.region(client.region)
69+
.timeout(Duration.ofSeconds(client.timeout.toLong()))
70+
.client(
71+
BedrockRuntimeAsyncClient.builder()
72+
.region(client.region)
73+
.credentialsProvider(StaticCredentialsProvider.create(credentials)).build())
74+
.defaultRequestParameters(
75+
BedrockChatRequestParameters.builder()
76+
.topP(client.topP)
77+
.topK(client.topK)
78+
.temperature(client.temperature.toDouble())
79+
.maxOutputTokens(client.maxOutputTokens)
80+
.build()
81+
)
82+
.build()
83+
}
84+
85+
fun saveToken(client: AmazonBedrockClientConfiguration, token: String) {
86+
cs.launch(Dispatchers.Default) {
87+
try {
88+
PasswordSafe.instance.setPassword(getCredentialAttributes(client.id), token)
89+
client.accessKeyIsStored = true
90+
} catch (e: Exception) {
91+
sendNotification(Notification.unableToSaveToken(e.message))
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)