Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,897 changes: 1,613 additions & 284 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"webpack-obfuscator": "^3.5.1"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^3.0.73",
"@ai-sdk/anthropic": "^2.0.47",
"@ai-sdk/azure": "^2.0.74",
"@ai-sdk/google": "^2.0.43",
Expand All @@ -168,6 +169,7 @@
"@ai-sdk/openai-compatible": "^1.0.27",
"@ai-sdk/perplexity": "^2.0.20",
"@ai-sdk/provider": "^2.0.0",
"@aws-sdk/client-bedrock": "^3.969.0",
"@braintree/sanitize-url": "^6.0.4",
"@capacitor-community/sqlite": "^6.0.2",
"@capacitor/android": "^6.1.1",
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/components/icons/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ export default function ProviderIcon(props: { className?: string; size?: number;
<path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path>
</>
)}
{provider === ModelProviderEnum.Bedrock && (
<>
<path d="M6.763 11.212c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 01-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 01-.287-.375 6.18 6.18 0 01-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272-.09.04-.184.075-.28.104a.488.488 0 01-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 01.224-.167 4.577 4.577 0 011.005-.36 4.84 4.84 0 011.246-.151c.95 0 1.644.216 2.091.647.44.43.662 1.085.662 1.963v2.586h.016zm-3.24 1.214c.263 0 .534-.048.822-.144a1.78 1.78 0 00.758-.51 1.27 1.27 0 00.272-.512c.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 00-.735-.136 6.02 6.02 0 00-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.398 1.398 0 01-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 01.32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 01.311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 01-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 01-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08l-.686.001zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 01-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 00.415-.758.777.777 0 00-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 01-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 01.24.2.43.43 0 01.071.263v.375c0 .168-.064.256-.184.256a.83.83 0 01-.303-.096 3.652 3.652 0 00-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926a2.157 2.157 0 01-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167z"></path>
<path d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351zm23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399z"></path>
</>
)}
</svg>
)
}
8 changes: 7 additions & 1 deletion src/renderer/hooks/useProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export const useProviders = () => {
models: chatboxAIModels,
}
} else if (
(!p.isCustom && providerSettings?.apiKey) ||
// For Bedrock, check AWS credentials instead of apiKey
(p.id === ModelProviderEnum.Bedrock &&
providerSettings?.awsAccessKeyId &&
providerSettings?.awsSecretAccessKey) ||
// For other non-custom providers, check apiKey
(!p.isCustom && p.id !== ModelProviderEnum.Bedrock && providerSettings?.apiKey) ||
// For custom providers, Ollama, and LMStudio, check models list
((p.isCustom || p.id === ModelProviderEnum.Ollama || p.id === ModelProviderEnum.LMStudio) &&
providerSettings?.models?.length)
) {
Expand Down
170 changes: 170 additions & 0 deletions src/renderer/packages/model-setting-utils/bedrock-setting-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { BedrockClient, ListInferenceProfilesCommand, ListFoundationModelsCommand } from '@aws-sdk/client-bedrock'
import type { ModelProvider, ProviderModelInfo, ProviderSettings, SessionType } from 'src/shared/types'
import { ModelProviderEnum } from 'src/shared/types'
import BaseConfig from './base-config'
import { ModelSettingUtil } from './interface'

export default class BedrockSettingUtil extends BaseConfig implements ModelSettingUtil {
public provider: ModelProvider = ModelProviderEnum.Bedrock

async getCurrentModelDisplayName(
model: string,
sessionType: SessionType,
providerSettings?: ProviderSettings
): Promise<string> {
return `AWS Bedrock (${providerSettings?.models?.find((m) => m.modelId === model)?.nickname || model})`
}

protected async listProviderModels(providerSettings?: ProviderSettings): Promise<ProviderModelInfo[]> {
try {
// v3.0.73 only supports: accessKeyId, secretAccessKey, sessionToken (optional)
// Create Bedrock client with credentials from settings
if (!providerSettings?.awsAccessKeyId || !providerSettings?.awsSecretAccessKey) {
console.warn('Bedrock: AWS Access Key ID and Secret Access Key are required')
return []
}

const clientConfig: any = {
region: providerSettings.awsRegion || 'us-east-1',
credentials: {
accessKeyId: providerSettings.awsAccessKeyId,
secretAccessKey: providerSettings.awsSecretAccessKey,
},
}

if (providerSettings.awsSessionToken) {
clientConfig.credentials.sessionToken = providerSettings.awsSessionToken
}

const client = new BedrockClient(clientConfig)

// Step 1: Fetch all foundation models to get capabilities info
const foundationModelsCommand = new ListFoundationModelsCommand({})
const foundationModelsResponse = await client.send(foundationModelsCommand)

// Build a map of foundation model ID to capabilities and limits
const modelCapabilitiesMap = new Map<
string,
{
hasVision: boolean
hasToolUse: boolean
hasReasoning: boolean
maxOutput: number
contextWindow: number
}
>()

foundationModelsResponse.modelSummaries?.forEach((model) => {
// Include models that support streaming and are ACTIVE/LEGACY
// Note: Claude 4+ models have inferenceTypesSupported: ["INFERENCE_PROFILE"] instead of ["ON_DEMAND"]
if (
model.modelId &&
model.responseStreamingSupported === true &&
(model.modelLifecycle?.status === 'ACTIVE' || model.modelLifecycle?.status === 'LEGACY')
) {
// Use detailed info from 'converse' field if available
const hasImageInput =
model.inputModalities?.includes('IMAGE') ||
(model as any).converse?.userImageTypesSupported?.length > 0 ||
false
const hasTextOutput = model.outputModalities?.includes('TEXT') || false
const hasToolUse = hasTextOutput // Most text models support tool use
const hasReasoning =
(model as any).converse?.reasoningSupported !== undefined ||
model.modelId.includes('sonnet-4') ||
model.modelId.includes('opus-4') ||
model.modelId.includes('claude-3-7')

// Extract token limits from converse field
const maxTokens = (model as any).converse?.maxTokensMaximum || 8_192

// Parse context window from description or use defaults
let contextWindow = 200_000 // Default
const maxContextStr = (model as any).description?.maxContextWindow
if (maxContextStr === '1M') {
contextWindow = 1_000_000
} else if (model.modelId.includes('claude-4') || model.modelId.includes('claude-3-7')) {
contextWindow = 200_000
} else if (model.modelId.includes('nova')) {
contextWindow = 300_000
}

modelCapabilitiesMap.set(model.modelId, {
hasVision: hasImageInput && hasTextOutput,
hasToolUse,
hasReasoning,
maxOutput: maxTokens,
contextWindow,
})
}
})

// Step 2: Fetch all inference profiles with pagination
const allProfiles: any[] = []
let nextToken: string | undefined = undefined

do {
const command: ListInferenceProfilesCommand = new ListInferenceProfilesCommand({
maxResults: 1000, // Max allowed by AWS API
nextToken,
})
const response: Awaited<ReturnType<typeof client.send>> = await client.send(command)

if (response.inferenceProfileSummaries) {
allProfiles.push(...response.inferenceProfileSummaries)
}

nextToken = (response as any).nextToken
} while (nextToken)

// Step 3: Convert inference profiles to ProviderModelInfo
const models: ProviderModelInfo[] = allProfiles
.filter((profile) => {
// Only include ACTIVE profiles
return profile.status === 'ACTIVE'
})
.map((profile) => {
// Extract foundation model ID from the first model ARN
// ARN format: arn:aws:bedrock:region::foundation-model/model.id
const firstModelArn = profile.models?.[0]?.modelArn
let foundationModelId: string | undefined

if (firstModelArn) {
const match = firstModelArn.match(/foundation-model\/(.+)$/)
if (match) {
foundationModelId = match[1]
}
}

// Get capabilities and limits from foundation model
const capabilities: ('vision' | 'tool_use' | 'reasoning')[] = []
let contextWindow = 200_000 // Default
let maxOutput = 8_192 // Default

if (foundationModelId && modelCapabilitiesMap.has(foundationModelId)) {
const caps = modelCapabilitiesMap.get(foundationModelId)!
if (caps.hasVision) capabilities.push('vision')
if (caps.hasToolUse) capabilities.push('tool_use')
if (caps.hasReasoning) capabilities.push('reasoning')
contextWindow = caps.contextWindow
maxOutput = caps.maxOutput
}

return {
modelId: profile.inferenceProfileId!,
nickname: profile.inferenceProfileName || profile.inferenceProfileId,
type: 'chat' as const,
capabilities,
contextWindow,
maxOutput,
}
})

return models
} catch (error) {
console.error('Failed to list Bedrock models:', error)
// Return empty array on error, let the UI show default models
return []
}
}
}
4 changes: 4 additions & 0 deletions src/renderer/packages/model-setting-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Settings,
} from 'src/shared/types'
import AzureSettingUtil from './azure-setting-util'
import BedrockSettingUtil from './bedrock-setting-util'
import ChatboxAISettingUtil from './chatboxai-setting-util'
import ChatGLMSettingUtil from './chatglm-setting-util'
import ClaudeSettingUtil from './claude-setting-util'
Expand All @@ -24,6 +25,7 @@ import MistralAISettingUtil from './mistral-ai-setting-util'
import OllamaSettingUtil from './ollama-setting-util'
import OpenAIResponsesSettingUtil from './openai-responses-setting-util'
import OpenAISettingUtil from './openai-setting-util'
import OpenRouterSettingUtil from './openrouter-setting-util'
import PerplexitySettingUtil from './perplexity-setting-util'
import SiliconFlowSettingUtil from './siliconflow-setting-util'
import VolcEngineSettingUtil from './volcengine-setting-util'
Expand All @@ -35,6 +37,7 @@ export function getModelSettingUtil(
): ModelSettingUtil {
const hash: Record<ModelProvider, new () => ModelSettingUtil> = {
[ModelProviderEnum.Azure]: AzureSettingUtil,
[ModelProviderEnum.Bedrock]: BedrockSettingUtil,
[ModelProviderEnum.ChatboxAI]: ChatboxAISettingUtil,
[ModelProviderEnum.ChatGLM6B]: ChatGLMSettingUtil,
[ModelProviderEnum.Claude]: ClaudeSettingUtil,
Expand All @@ -43,6 +46,7 @@ export function getModelSettingUtil(
[ModelProviderEnum.Ollama]: OllamaSettingUtil,
[ModelProviderEnum.OpenAI]: OpenAISettingUtil,
[ModelProviderEnum.OpenAIResponses]: OpenAIResponsesSettingUtil,
[ModelProviderEnum.OpenRouter]: OpenRouterSettingUtil,
[ModelProviderEnum.DeepSeek]: DeepSeekSettingUtil,
[ModelProviderEnum.SiliconFlow]: SiliconFlowSettingUtil,
[ModelProviderEnum.VolcEngine]: VolcEngineSettingUtil,
Expand Down
91 changes: 86 additions & 5 deletions src/renderer/routes/settings/provider/$providerId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,23 @@ function ProviderSettings({ providerId }: { providerId: string }) {
const checkModel =
selectedTestModel || baseInfo?.defaultSettings?.models?.[0]?.modelId || providerSettings?.models?.[0]?.modelId

// Check if credentials are valid for connection test
const hasValidCredentials = () => {
// For Bedrock, check if AWS Access Key ID and Secret Access Key are present
if (baseInfo.id === ModelProviderEnum.Bedrock) {
return !!providerSettings?.awsAccessKeyId && !!providerSettings?.awsSecretAccessKey
}
// For other providers, check API key
return !!providerSettings?.apiKey
}

const getCredentialTooltipMessage = () => {
if (baseInfo.id === ModelProviderEnum.Bedrock) {
return t('AWS Access Key ID and Secret Access Key are required to check connection')
}
return t('API Key is required to check connection')
}

const handleCheckApiKey = async (modelId?: string) => {
const testModel = modelId || checkModel
if (!testModel) return
Expand Down Expand Up @@ -368,26 +385,28 @@ function ProviderSettings({ providerId }: { providerId: string }) {
)}

{/* API Key */}
{![ModelProviderEnum.Ollama, ModelProviderEnum.LMStudio, ''].includes(baseInfo.id) && (
{![ModelProviderEnum.Ollama, ModelProviderEnum.LMStudio, ModelProviderEnum.Bedrock, ''].includes(
baseInfo.id
) && (
<Stack gap="xxs">
<Text span fw="600">
{t('API Key')}
</Text>
<Flex gap="xs" align="center">
<PasswordInput flex={1} value={providerSettings?.apiKey || ''} onChange={handleApiKeyChange} />
<Tooltip
disabled={!!providerSettings?.apiKey && displayModels.length > 0}
disabled={hasValidCredentials() && displayModels.length > 0}
label={
!providerSettings?.apiKey
? t('API Key is required to check connection')
!hasValidCredentials()
? getCredentialTooltipMessage()
: displayModels.length === 0
? t('Add at least one model to check connection')
: null
}
>
<Button
size="sm"
disabled={!providerSettings?.apiKey || displayModels.length === 0}
disabled={!hasValidCredentials() || displayModels.length === 0}
loading={modelTestResult?.testing || false}
onClick={() => setShowTestModelSelector(true)}
>
Expand Down Expand Up @@ -589,6 +608,68 @@ function ProviderSettings({ providerId }: { providerId: string }) {
</>
)}

{/* AWS Bedrock specific fields */}
{baseInfo.id === ModelProviderEnum.Bedrock && (
<>
{/* AWS Access Key ID */}
<Stack gap="xxs">
<Text span fw="600">
{t('AWS Access Key ID')}
</Text>
<Flex gap="xs" align="center">
<PasswordInput
flex={1}
value={providerSettings?.awsAccessKeyId || ''}
placeholder="AKIAIOSFODNN7EXAMPLE"
onChange={(e) =>
setProviderSettings({
awsAccessKeyId: e.currentTarget.value,
})
}
/>
</Flex>
</Stack>

{/* AWS Secret Access Key */}
<Stack gap="xxs">
<Text span fw="600">
{t('AWS Secret Access Key')}
</Text>
<Flex gap="xs" align="center">
<PasswordInput
flex={1}
value={providerSettings?.awsSecretAccessKey || ''}
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
onChange={(e) =>
setProviderSettings({
awsSecretAccessKey: e.currentTarget.value,
})
}
/>
</Flex>
</Stack>

{/* AWS Region */}
<Stack gap="xxs">
<Text span fw="600">
{t('AWS Region')}
</Text>
<Flex gap="xs" align="center">
<TextInput
flex={1}
value={providerSettings?.awsRegion || ''}
placeholder="us-east-1"
onChange={(e) =>
setProviderSettings({
awsRegion: e.currentTarget.value,
})
}
/>
</Flex>
</Stack>
</>
)}

{/* Models */}
<Stack gap="xxs">
<Flex justify="space-between" align="center">
Expand Down
Binary file added src/renderer/static/icons/providers/bedrock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/renderer/stores/settingActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export function needEditSetting() {
if (keys.filter((key) => !!providers[key].apiKey).length > 0) {
return false
}
// Bedrock 配置了 AWS 凭证
if (
providers[ModelProviderEnum.Bedrock]?.awsAccessKeyId &&
providers[ModelProviderEnum.Bedrock]?.awsSecretAccessKey
) {
return false
}
// Ollama / LMStudio/ custom provider 配置了至少一个模型
if (
keys.filter(
Expand Down
Loading