feat: amazon bedrock request use bedrock api key (#10727)

* feat: amazon bedrock request use bedrock api key

* feat: ai-core/provider support bedrock api key

* refactor: extract AWS Bedrock auth type and remove redundant state

* feat: add bedrock reasoning support

Add AWS Bedrock-specific reasoning parameter handling to support Extended Thinking feature for Claude models via Bedrock API.

Changes:
- Add `buildBedrockProviderOptions` function in options.ts to handle Bedrock-specific provider options
- Add `getBedrockReasoningParams` function in reasoning.ts to generate reasoning config with budget tokens
- Register 'bedrock' case in provider options switch to route to Bedrock-specific builder
- Reuse `getAnthropicThinkingBudget` helper for consistent token budget calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: add migration for Bedrock auth type and API key fields

* refactor: replace any type with BedrockRuntimeClientConfig in AWS Bedrock client

* fix: bug fix

* fix: lint error

* fix: bedrock reasoning

* chore: bump persisted reducer version to 171

* Update src/renderer/src/store/migrate.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
Zephyr 2025-11-03 21:05:10 +08:00 committed by GitHub
parent 49bd298d37
commit abd5d3b96f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1120 additions and 2022 deletions

View File

@ -113,9 +113,9 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0",
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
"@aws-sdk/client-bedrock": "^3.840.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/client-s3": "^3.840.0",
"@aws-sdk/client-bedrock": "^3.910.0",
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
"@aws-sdk/client-s3": "^3.910.0",
"@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
"@cherrystudio/embedjs": "^0.1.31",

View File

@ -1,6 +1,7 @@
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock'
import {
BedrockRuntimeClient,
type BedrockRuntimeClientConfig,
ConverseCommand,
InvokeModelCommand,
InvokeModelWithResponseStreamCommand
@ -11,6 +12,8 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
@ -75,32 +78,48 @@ export class AwsBedrockAPIClient extends BaseApiClient<
}
const region = getAwsBedrockRegion()
const accessKeyId = getAwsBedrockAccessKeyId()
const secretAccessKey = getAwsBedrockSecretAccessKey()
const authType = getAwsBedrockAuthType()
if (!region) {
throw new Error('AWS region is required. Please configure AWS-Region in extra headers.')
throw new Error('AWS region is required. Please configure AWS region in settings.')
}
if (!accessKeyId || !secretAccessKey) {
throw new Error('AWS credentials are required. Please configure AWS-Access-Key-ID and AWS-Secret-Access-Key.')
// Build client configuration based on auth type
let clientConfig: BedrockRuntimeClientConfig
if (authType === 'iam') {
// IAM credentials authentication
const accessKeyId = getAwsBedrockAccessKeyId()
const secretAccessKey = getAwsBedrockSecretAccessKey()
if (!accessKeyId || !secretAccessKey) {
throw new Error('AWS credentials are required. Please configure Access Key ID and Secret Access Key.')
}
clientConfig = {
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
} else {
// API Key authentication
const awsBedrockApiKey = getAwsBedrockApiKey()
if (!awsBedrockApiKey) {
throw new Error('AWS Bedrock API Key is required. Please configure API Key in settings.')
}
clientConfig = {
region,
token: { token: awsBedrockApiKey },
authSchemePreference: ['httpBearerAuth']
}
}
const client = new BedrockRuntimeClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
const bedrockClient = new BedrockClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
const client = new BedrockRuntimeClient(clientConfig)
const bedrockClient = new BedrockClient(clientConfig)
this.sdkInstance = { client, bedrockClient, region }
return this.sdkInstance

View File

@ -14,6 +14,8 @@ import {
} from '@renderer/config/providers'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
@ -192,9 +194,15 @@ export function providerToAiSdkConfig(
// bedrock
if (aiSdkProviderId === 'bedrock') {
const authType = getAwsBedrockAuthType()
extraOptions.region = getAwsBedrockRegion()
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
if (authType === 'apiKey') {
extraOptions.apiKey = getAwsBedrockApiKey()
} else {
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
}
}
// google-vertex
if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') {

View File

@ -17,6 +17,7 @@ import { getAiSdkProviderId } from '../provider/factory'
import { buildGeminiGenerateImageParams } from './image'
import {
getAnthropicReasoningParams,
getBedrockReasoningParams,
getCustomParameters,
getGeminiReasoningParams,
getOpenAIReasoningParams,
@ -127,6 +128,9 @@ export function buildProviderOptions(
case 'google-vertex-anthropic':
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
break
case 'bedrock':
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
break
default:
// 对于其他 provider使用通用的构建逻辑
providerSpecificOptions = {
@ -266,6 +270,32 @@ function buildXAIProviderOptions(
return providerOptions
}
/**
* Build Bedrock providerOptions
*/
function buildBedrockProviderOptions(
assistant: Assistant,
model: Model,
capabilities: {
enableReasoning: boolean
enableWebSearch: boolean
enableGenerateImage: boolean
}
): Record<string, any> {
const { enableReasoning } = capabilities
let providerOptions: Record<string, any> = {}
if (enableReasoning) {
const reasoningParams = getBedrockReasoningParams(assistant, model)
providerOptions = {
...providerOptions,
...reasoningParams
}
}
return providerOptions
}
/**
* providerOptions provider
*/

View File

@ -485,6 +485,34 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
}
}
/**
* Get Bedrock reasoning parameters
*/
export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
if (!isReasoningModel(model)) {
return {}
}
const reasoningEffort = assistant?.settings?.reasoning_effort
if (reasoningEffort === undefined) {
return {}
}
// Only apply thinking budget for Claude reasoning models
if (!isSupportedThinkingTokenClaudeModel(model)) {
return {}
}
const budgetTokens = getAnthropicThinkingBudget(assistant, model)
return {
reasoningConfig: {
type: 'enabled',
budgetTokens: budgetTokens
}
}
}
/**
*
* assistant

View File

@ -1,5 +1,12 @@
import store, { useAppSelector } from '@renderer/store'
import { setAwsBedrockAccessKeyId, setAwsBedrockRegion, setAwsBedrockSecretAccessKey } from '@renderer/store/llm'
import {
setAwsBedrockAccessKeyId,
setAwsBedrockApiKey,
setAwsBedrockAuthType,
setAwsBedrockRegion,
setAwsBedrockSecretAccessKey
} from '@renderer/store/llm'
import type { AwsBedrockAuthType } from '@renderer/types'
import { useDispatch } from 'react-redux'
export function useAwsBedrockSettings() {
@ -8,8 +15,10 @@ export function useAwsBedrockSettings() {
return {
...settings,
setAuthType: (authType: AwsBedrockAuthType) => dispatch(setAwsBedrockAuthType(authType)),
setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)),
setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)),
setApiKey: (apiKey: string) => dispatch(setAwsBedrockApiKey(apiKey)),
setRegion: (region: string) => dispatch(setAwsBedrockRegion(region))
}
}
@ -18,6 +27,10 @@ export function getAwsBedrockSettings() {
return store.getState().llm.settings.awsBedrock
}
export function getAwsBedrockAuthType() {
return store.getState().llm.settings.awsBedrock.authType
}
export function getAwsBedrockAccessKeyId() {
return store.getState().llm.settings.awsBedrock.accessKeyId
}
@ -26,6 +39,10 @@ export function getAwsBedrockSecretAccessKey() {
return store.getState().llm.settings.awsBedrock.secretAccessKey
}
export function getAwsBedrockApiKey() {
return store.getState().llm.settings.awsBedrock.apiKey
}
export function getAwsBedrockRegion() {
return store.getState().llm.settings.awsBedrock.region
}

View File

@ -4260,6 +4260,12 @@
"aws-bedrock": {
"access_key_id": "AWS Access Key ID",
"access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services",
"api_key": "Bedrock API Key",
"api_key_help": "Your AWS Bedrock API Key for authentication",
"auth_type": "Authentication Type",
"auth_type_api_key": "Bedrock API Key",
"auth_type_help": "Choose between IAM credentials or Bedrock API Key authentication",
"auth_type_iam": "IAM Credentials",
"description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models",
"region": "AWS Region",
"region_help": "Your AWS service region, e.g., us-east-1",

View File

@ -4260,6 +4260,12 @@
"aws-bedrock": {
"access_key_id": "AWS 访问密钥 ID",
"access_key_id_help": "您的 AWS 访问密钥 ID用于访问 AWS Bedrock 服务",
"api_key": "Bedrock API 密钥",
"api_key_help": "您的 AWS Bedrock API 密钥,用于身份验证",
"auth_type": "认证方式",
"auth_type_api_key": "Bedrock API 密钥",
"auth_type_help": "选择使用 IAM 凭证或 Bedrock API 密钥进行身份验证",
"auth_type_iam": "IAM 凭证",
"description": "AWS Bedrock 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型",
"region": "AWS 区域",
"region_help": "您的 AWS 服务区域,例如 us-east-1",

View File

@ -4260,6 +4260,12 @@
"aws-bedrock": {
"access_key_id": "AWS 存取密鑰 ID",
"access_key_id_help": "您的 AWS 存取密鑰 ID用於存取 AWS Bedrock 服務",
"api_key": "Bedrock API 金鑰",
"api_key_help": "您的 AWS Bedrock API 金鑰,用於身份驗證",
"auth_type": "認證方式",
"auth_type_api_key": "Bedrock API 金鑰",
"auth_type_help": "選擇使用 IAM 憑證或 Bedrock API 金鑰進行身份驗證",
"auth_type_iam": "IAM 憑證",
"description": "AWS Bedrock 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型",
"region": "AWS 區域",
"region_help": "您的 AWS 服務區域,例如 us-east-1",

View File

@ -1,7 +1,7 @@
import { HStack } from '@renderer/components/Layout'
import { PROVIDER_URLS } from '@renderer/config/providers'
import { useAwsBedrockSettings } from '@renderer/hooks/useAwsBedrock'
import { Alert, Input } from 'antd'
import { Alert, Input, Radio } from 'antd'
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -10,14 +10,25 @@ import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle }
const AwsBedrockSettings: FC = () => {
const { t } = useTranslation()
const { accessKeyId, secretAccessKey, region, setAccessKeyId, setSecretAccessKey, setRegion } =
useAwsBedrockSettings()
const {
authType,
accessKeyId,
secretAccessKey,
apiKey,
region,
setAuthType,
setAccessKeyId,
setSecretAccessKey,
setApiKey,
setRegion
} = useAwsBedrockSettings()
const providerConfig = PROVIDER_URLS['aws-bedrock']
const apiKeyWebsite = providerConfig?.websites?.apiKey
const [localAccessKeyId, setLocalAccessKeyId] = useState(accessKeyId)
const [localSecretAccessKey, setLocalSecretAccessKey] = useState(secretAccessKey)
const [localApiKey, setLocalApiKey] = useState(apiKey)
const [localRegion, setLocalRegion] = useState(region)
return (
@ -25,39 +36,75 @@ const AwsBedrockSettings: FC = () => {
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.title')}</SettingSubtitle>
<Alert type="info" style={{ marginTop: 5 }} message={t('settings.provider.aws-bedrock.description')} showIcon />
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.access_key_id')}</SettingSubtitle>
<Input
value={localAccessKeyId}
placeholder="Access Key ID"
onChange={(e) => setLocalAccessKeyId(e.target.value)}
onBlur={() => setAccessKeyId(localAccessKeyId)}
style={{ marginTop: 5 }}
/>
{/* Authentication Type Selector */}
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.auth_type')}</SettingSubtitle>
<Radio.Group value={authType} onChange={(e) => setAuthType(e.target.value)} style={{ marginTop: 5 }}>
<Radio value="iam">{t('settings.provider.aws-bedrock.auth_type_iam')}</Radio>
<Radio value="apiKey">{t('settings.provider.aws-bedrock.auth_type_api_key')}</Radio>
</Radio.Group>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.aws-bedrock.access_key_id_help')}</SettingHelpText>
<SettingHelpText>{t('settings.provider.aws-bedrock.auth_type_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.secret_access_key')}</SettingSubtitle>
<Input.Password
value={localSecretAccessKey}
placeholder="Secret Access Key"
onChange={(e) => setLocalSecretAccessKey(e.target.value)}
onBlur={() => setSecretAccessKey(localSecretAccessKey)}
style={{ marginTop: 5 }}
spellCheck={false}
/>
{apiKeyWebsite && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
</HStack>
<SettingHelpText>{t('settings.provider.aws-bedrock.secret_access_key_help')}</SettingHelpText>
</SettingHelpTextRow>
{/* IAM Credentials Fields */}
{authType === 'iam' && (
<>
<SettingSubtitle style={{ marginTop: 15 }}>
{t('settings.provider.aws-bedrock.access_key_id')}
</SettingSubtitle>
<Input
value={localAccessKeyId}
placeholder="Access Key ID"
onChange={(e) => setLocalAccessKeyId(e.target.value)}
onBlur={() => setAccessKeyId(localAccessKeyId)}
style={{ marginTop: 5 }}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.aws-bedrock.access_key_id_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 15 }}>
{t('settings.provider.aws-bedrock.secret_access_key')}
</SettingSubtitle>
<Input.Password
value={localSecretAccessKey}
placeholder="Secret Access Key"
onChange={(e) => setLocalSecretAccessKey(e.target.value)}
onBlur={() => setSecretAccessKey(localSecretAccessKey)}
style={{ marginTop: 5 }}
spellCheck={false}
/>
{apiKeyWebsite && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
</HStack>
<SettingHelpText>{t('settings.provider.aws-bedrock.secret_access_key_help')}</SettingHelpText>
</SettingHelpTextRow>
)}
</>
)}
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.aws-bedrock.region')}</SettingSubtitle>
{authType === 'apiKey' && (
<>
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.api_key')}</SettingSubtitle>
<Input.Password
value={localApiKey}
placeholder="Bedrock API Key"
onChange={(e) => setLocalApiKey(e.target.value)}
onBlur={() => setApiKey(localApiKey)}
style={{ marginTop: 5 }}
spellCheck={false}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.aws-bedrock.api_key_help')}</SettingHelpText>
</SettingHelpTextRow>
</>
)}
<SettingSubtitle style={{ marginTop: 15 }}>{t('settings.provider.aws-bedrock.region')}</SettingSubtitle>
<Input
value={localRegion}
placeholder="us-east-1"

View File

@ -230,8 +230,10 @@ vi.mock('@renderer/store/llm.ts', () => {
location: ''
},
awsBedrock: {
authType: 'iam',
accessKeyId: '',
secretAccessKey: '',
apiKey: '',
region: ''
}
}

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 170,
version: 171,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate
},

View File

@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'
import { isLocalAi } from '@renderer/config/env'
import { SYSTEM_MODELS } from '@renderer/config/models'
import { SYSTEM_PROVIDERS } from '@renderer/config/providers'
import type { Model, Provider } from '@renderer/types'
import type { AwsBedrockAuthType, Model, Provider } from '@renderer/types'
import { uniqBy } from 'lodash'
type LlmSettings = {
@ -25,8 +25,10 @@ type LlmSettings = {
location: string
}
awsBedrock: {
authType: AwsBedrockAuthType
accessKeyId: string
secretAccessKey: string
apiKey: string
region: string
}
}
@ -68,8 +70,10 @@ export const initialState: LlmState = {
location: ''
},
awsBedrock: {
authType: 'iam',
accessKeyId: '',
secretAccessKey: '',
apiKey: '',
region: ''
}
}
@ -197,12 +201,18 @@ const llmSlice = createSlice({
setVertexAIServiceAccountClientEmail: (state, action: PayloadAction<string>) => {
state.settings.vertexai.serviceAccount.clientEmail = action.payload
},
setAwsBedrockAuthType: (state, action: PayloadAction<AwsBedrockAuthType>) => {
state.settings.awsBedrock.authType = action.payload
},
setAwsBedrockAccessKeyId: (state, action: PayloadAction<string>) => {
state.settings.awsBedrock.accessKeyId = action.payload
},
setAwsBedrockSecretAccessKey: (state, action: PayloadAction<string>) => {
state.settings.awsBedrock.secretAccessKey = action.payload
},
setAwsBedrockApiKey: (state, action: PayloadAction<string>) => {
state.settings.awsBedrock.apiKey = action.payload
},
setAwsBedrockRegion: (state, action: PayloadAction<string>) => {
state.settings.awsBedrock.region = action.payload
},
@ -242,8 +252,10 @@ export const {
setVertexAILocation,
setVertexAIServiceAccountPrivateKey,
setVertexAIServiceAccountClientEmail,
setAwsBedrockAuthType,
setAwsBedrockAccessKeyId,
setAwsBedrockSecretAccessKey,
setAwsBedrockApiKey,
setAwsBedrockRegion,
updateModel
} = llmSlice.actions

View File

@ -2794,6 +2794,17 @@ const migrateConfig = {
logger.error('migrate 170 error', error as Error)
return state
}
},
'171': (state: RootState) => {
try {
addProvider(state, 'sophnet')
state.llm.providers = moveProvider(state.llm.providers, 'sophnet', 17)
state.settings.defaultPaintingProvider = 'cherryin'
return state
} catch (error) {
logger.error('migrate 171 error', error as Error)
return state
}
}
}

View File

@ -73,6 +73,17 @@ export function isServiceTier(tier: string): tier is ServiceTier {
return isGroqServiceTier(tier) || isOpenAIServiceTier(tier)
}
export const AwsBedrockAuthTypes = {
iam: 'iam',
apiKey: 'apiKey'
} as const
export type AwsBedrockAuthType = keyof typeof AwsBedrockAuthTypes
export function isAwsBedrockAuthType(type: string): type is AwsBedrockAuthType {
return Object.hasOwn(AwsBedrockAuthTypes, type)
}
export type Provider = {
id: string
type: ProviderType

2821
yarn.lock

File diff suppressed because it is too large Load Diff