Skip to content

Commit e1cfbcb

Browse files
committed
🤖 feat: improve Bedrock provider UX
- Add AWS icon for Bedrock models in chat (reusing existing aws.svg) - Parse Bedrock model names (e.g., global.anthropic.claude-sonnet-4-5-20250929-v1:0 → Sonnet 4.5) - Handle both old (claude-3-5-sonnet) and new (claude-sonnet-4-5) naming formats - Show Bedrock-specific configuration help in Settings instead of API Key/Base URL fields - Add /provider bedrock suggestions with region, bedrockApiKey, accessKeyId, secretAccessKey - Support bedrockApiKey in providers.jsonc (maps to AWS_BEARER_TOKEN_BEDROCK) _Generated with mux_
1 parent f11f68c commit e1cfbcb

File tree

9 files changed

+348
-136
lines changed

9 files changed

+348
-136
lines changed
Lines changed: 1 addition & 0 deletions
Loading

‎src/browser/components/Messages/ModelDisplay.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import AnthropicIcon from "@/browser/assets/icons/anthropic.svg?react";
33
import OpenAIIcon from "@/browser/assets/icons/openai.svg?react";
4+
import AWSIcon from "@/browser/assets/icons/aws.svg?react";
45
import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip";
56
import { formatModelDisplayName } from "@/common/utils/ai/modelDisplay";
67

@@ -29,6 +30,8 @@ export const ModelDisplay: React.FC<ModelDisplayProps> = ({ modelString, showToo
2930
return <AnthropicIcon />;
3031
case "openai":
3132
return <OpenAIIcon />;
33+
case "bedrock":
34+
return <AWSIcon />;
3235
default:
3336
return null;
3437
}

‎src/browser/components/Settings/sections/ProvidersSection.tsx‎

Lines changed: 172 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,67 @@ import React, { useState, useEffect, useCallback } from "react";
22
import { ChevronDown, ChevronRight, Check, X } from "lucide-react";
33
import type { ProvidersConfigMap } from "../types";
44
import { SUPPORTED_PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers";
5+
import type { ProviderName } from "@/common/constants/providers";
6+
7+
interface FieldConfig {
8+
key: string;
9+
label: string;
10+
placeholder: string;
11+
type: "secret" | "text";
12+
optional?: boolean;
13+
}
14+
15+
/**
16+
* Get provider-specific field configuration.
17+
* Most providers use API Key + Base URL, but some (like Bedrock) have different needs.
18+
*/
19+
function getProviderFields(provider: ProviderName): FieldConfig[] {
20+
if (provider === "bedrock") {
21+
return [
22+
{ key: "region", label: "Region", placeholder: "us-east-1", type: "text" },
23+
{
24+
key: "bearerToken",
25+
label: "Bearer Token",
26+
placeholder: "AWS_BEARER_TOKEN_BEDROCK",
27+
type: "secret",
28+
optional: true,
29+
},
30+
{
31+
key: "accessKeyId",
32+
label: "Access Key ID",
33+
placeholder: "AWS Access Key ID",
34+
type: "secret",
35+
optional: true,
36+
},
37+
{
38+
key: "secretAccessKey",
39+
label: "Secret Access Key",
40+
placeholder: "AWS Secret Access Key",
41+
type: "secret",
42+
optional: true,
43+
},
44+
];
45+
}
46+
47+
// Default for most providers
48+
return [
49+
{ key: "apiKey", label: "API Key", placeholder: "Enter API key", type: "secret" },
50+
{
51+
key: "baseUrl",
52+
label: "Base URL",
53+
placeholder: "https://api.example.com",
54+
type: "text",
55+
optional: true,
56+
},
57+
];
58+
}
559

660
export function ProvidersSection() {
761
const [config, setConfig] = useState<ProvidersConfigMap>({});
862
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
963
const [editingField, setEditingField] = useState<{
1064
provider: string;
11-
field: "apiKey" | "baseUrl";
65+
field: string;
1266
} | null>(null);
1367
const [editValue, setEditValue] = useState("");
1468
const [saving, setSaving] = useState(false);
@@ -26,11 +80,14 @@ export function ProvidersSection() {
2680
setEditingField(null);
2781
};
2882

29-
const handleStartEdit = (provider: string, field: "apiKey" | "baseUrl") => {
83+
const handleStartEdit = (provider: string, field: string, fieldConfig: FieldConfig) => {
3084
setEditingField({ provider, field });
31-
// For API key, start empty since we only show masked value
32-
// For baseUrl, show current value
33-
setEditValue(field === "baseUrl" ? (config[provider]?.baseUrl ?? "") : "");
85+
// For secrets, start empty since we only show masked value
86+
// For text fields, show current value
87+
const currentValue = (config[provider] as Record<string, unknown> | undefined)?.[field];
88+
setEditValue(
89+
fieldConfig.type === "text" && typeof currentValue === "string" ? currentValue : ""
90+
);
3491
};
3592

3693
const handleCancelEdit = () => {
@@ -44,8 +101,7 @@ export function ProvidersSection() {
44101
setSaving(true);
45102
try {
46103
const { provider, field } = editingField;
47-
const keyPath = field === "apiKey" ? ["apiKey"] : ["baseUrl"];
48-
await window.api.providers.setProviderConfig(provider, keyPath, editValue);
104+
await window.api.providers.setProviderConfig(provider, [field], editValue);
49105

50106
// Refresh config
51107
const cfg = await window.api.providers.getConfig();
@@ -57,19 +113,52 @@ export function ProvidersSection() {
57113
}
58114
}, [editingField, editValue]);
59115

60-
const handleClearBaseUrl = useCallback(async (provider: string) => {
116+
const handleClearField = useCallback(async (provider: string, field: string) => {
61117
setSaving(true);
62118
try {
63-
await window.api.providers.setProviderConfig(provider, ["baseUrl"], "");
119+
await window.api.providers.setProviderConfig(provider, [field], "");
64120
const cfg = await window.api.providers.getConfig();
65121
setConfig(cfg);
66122
} finally {
67123
setSaving(false);
68124
}
69125
}, []);
70126

71-
const isConfigured = (provider: string) => {
72-
return config[provider]?.apiKeySet ?? false;
127+
const isConfigured = (provider: string): boolean => {
128+
const providerConfig = config[provider];
129+
if (!providerConfig) return false;
130+
131+
// For Bedrock, check if any credential field is set
132+
if (provider === "bedrock") {
133+
return !!(
134+
providerConfig.region ||
135+
providerConfig.bearerTokenSet ||
136+
providerConfig.accessKeyIdSet ||
137+
providerConfig.secretAccessKeySet
138+
);
139+
}
140+
141+
// For other providers, check apiKeySet
142+
return providerConfig.apiKeySet ?? false;
143+
};
144+
145+
const getFieldValue = (provider: string, field: string): string | undefined => {
146+
const providerConfig = config[provider] as Record<string, unknown> | undefined;
147+
if (!providerConfig) return undefined;
148+
const value = providerConfig[field];
149+
return typeof value === "string" ? value : undefined;
150+
};
151+
152+
const isFieldSet = (provider: string, field: string, fieldConfig: FieldConfig): boolean => {
153+
if (fieldConfig.type === "secret") {
154+
// For apiKey, we have apiKeySet from the sanitized config
155+
if (field === "apiKey") return config[provider]?.apiKeySet ?? false;
156+
// For other secrets, check if the field exists in the raw config
157+
// Since we don't expose secret values, we assume they're not set if undefined
158+
const providerConfig = config[provider] as Record<string, unknown> | undefined;
159+
return providerConfig?.[`${field}Set`] === true;
160+
}
161+
return !!getFieldValue(provider, field);
73162
};
74163

75164
return (
@@ -81,8 +170,8 @@ export function ProvidersSection() {
81170

82171
{SUPPORTED_PROVIDERS.map((provider) => {
83172
const isExpanded = expandedProvider === provider;
84-
const providerConfig = config[provider];
85173
const configured = isConfigured(provider);
174+
const fields = getProviderFields(provider);
86175

87176
return (
88177
<div
@@ -114,117 +203,83 @@ export function ProvidersSection() {
114203
{/* Provider settings */}
115204
{isExpanded && (
116205
<div className="border-border-medium space-y-3 border-t px-4 py-3">
117-
{/* API Key */}
118-
<div>
119-
<label className="text-muted mb-1 block text-xs">API Key</label>
120-
{editingField?.provider === provider && editingField?.field === "apiKey" ? (
121-
<div className="flex gap-2">
122-
<input
123-
type="password"
124-
value={editValue}
125-
onChange={(e) => setEditValue(e.target.value)}
126-
placeholder="Enter API key"
127-
className="bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
128-
autoFocus
129-
onKeyDown={(e) => {
130-
if (e.key === "Enter") void handleSaveEdit();
131-
if (e.key === "Escape") handleCancelEdit();
132-
}}
133-
/>
134-
<button
135-
type="button"
136-
onClick={() => void handleSaveEdit()}
137-
disabled={saving}
138-
className="p-1 text-green-500 hover:text-green-400"
139-
>
140-
<Check className="h-4 w-4" />
141-
</button>
142-
<button
143-
type="button"
144-
onClick={handleCancelEdit}
145-
className="text-muted hover:text-foreground p-1"
146-
>
147-
<X className="h-4 w-4" />
148-
</button>
149-
</div>
150-
) : (
151-
<div className="flex items-center justify-between">
152-
<span className="text-foreground font-mono text-xs">
153-
{providerConfig?.apiKeySet ? "••••••••" : "Not set"}
154-
</span>
155-
<button
156-
type="button"
157-
onClick={() => handleStartEdit(provider, "apiKey")}
158-
className="text-accent hover:text-accent-light text-xs"
159-
>
160-
{providerConfig?.apiKeySet ? "Change" : "Set"}
161-
</button>
162-
</div>
163-
)}
164-
</div>
165-
166-
{/* Base URL (optional) */}
167-
<div>
168-
<label className="text-muted mb-1 block text-xs">
169-
Base URL <span className="text-dim">(optional)</span>
170-
</label>
171-
{editingField?.provider === provider && editingField?.field === "baseUrl" ? (
172-
<div className="flex gap-2">
173-
<input
174-
type="text"
175-
value={editValue}
176-
onChange={(e) => setEditValue(e.target.value)}
177-
placeholder="https://api.example.com"
178-
className="bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
179-
autoFocus
180-
onKeyDown={(e) => {
181-
if (e.key === "Enter") void handleSaveEdit();
182-
if (e.key === "Escape") handleCancelEdit();
183-
}}
184-
/>
185-
<button
186-
type="button"
187-
onClick={() => void handleSaveEdit()}
188-
disabled={saving}
189-
className="p-1 text-green-500 hover:text-green-400"
190-
>
191-
<Check className="h-4 w-4" />
192-
</button>
193-
<button
194-
type="button"
195-
onClick={handleCancelEdit}
196-
className="text-muted hover:text-foreground p-1"
197-
>
198-
<X className="h-4 w-4" />
199-
</button>
200-
</div>
201-
) : (
202-
<div className="flex items-center justify-between">
203-
<span className="text-foreground font-mono text-xs">
204-
{providerConfig?.baseUrl ?? "Default"}
205-
</span>
206-
<div className="flex gap-2">
207-
{providerConfig?.baseUrl && (
206+
{fields.map((fieldConfig) => {
207+
const isEditing =
208+
editingField?.provider === provider && editingField?.field === fieldConfig.key;
209+
const fieldValue = getFieldValue(provider, fieldConfig.key);
210+
const fieldIsSet = isFieldSet(provider, fieldConfig.key, fieldConfig);
211+
212+
return (
213+
<div key={fieldConfig.key}>
214+
<label className="text-muted mb-1 block text-xs">
215+
{fieldConfig.label}
216+
{fieldConfig.optional && <span className="text-dim"> (optional)</span>}
217+
</label>
218+
{isEditing ? (
219+
<div className="flex gap-2">
220+
<input
221+
type={fieldConfig.type === "secret" ? "password" : "text"}
222+
value={editValue}
223+
onChange={(e) => setEditValue(e.target.value)}
224+
placeholder={fieldConfig.placeholder}
225+
className="bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
226+
autoFocus
227+
onKeyDown={(e) => {
228+
if (e.key === "Enter") void handleSaveEdit();
229+
if (e.key === "Escape") handleCancelEdit();
230+
}}
231+
/>
208232
<button
209233
type="button"
210-
onClick={() => void handleClearBaseUrl(provider)}
234+
onClick={() => void handleSaveEdit()}
211235
disabled={saving}
212-
className="text-muted hover:text-error text-xs"
236+
className="p-1 text-green-500 hover:text-green-400"
237+
>
238+
<Check className="h-4 w-4" />
239+
</button>
240+
<button
241+
type="button"
242+
onClick={handleCancelEdit}
243+
className="text-muted hover:text-foreground p-1"
213244
>
214-
Clear
245+
<X className="h-4 w-4" />
215246
</button>
216-
)}
217-
<button
218-
type="button"
219-
onClick={() => handleStartEdit(provider, "baseUrl")}
220-
className="text-accent hover:text-accent-light text-xs"
221-
>
222-
{providerConfig?.baseUrl ? "Change" : "Set"}
223-
</button>
224-
</div>
247+
</div>
248+
) : (
249+
<div className="flex items-center justify-between">
250+
<span className="text-foreground font-mono text-xs">
251+
{fieldConfig.type === "secret"
252+
? fieldIsSet
253+
? "••••••••"
254+
: "Not set"
255+
: (fieldValue ?? "Default")}
256+
</span>
257+
<div className="flex gap-2">
258+
{fieldConfig.type === "text" && fieldValue && (
259+
<button
260+
type="button"
261+
onClick={() => void handleClearField(provider, fieldConfig.key)}
262+
disabled={saving}
263+
className="text-muted hover:text-error text-xs"
264+
>
265+
Clear
266+
</button>
267+
)}
268+
<button
269+
type="button"
270+
onClick={() =>
271+
handleStartEdit(provider, fieldConfig.key, fieldConfig)
272+
}
273+
className="text-accent hover:text-accent-light text-xs"
274+
>
275+
{fieldIsSet || fieldValue ? "Change" : "Set"}
276+
</button>
277+
</div>
278+
</div>
279+
)}
225280
</div>
226-
)}
227-
</div>
281+
);
282+
})}
228283
</div>
229284
)}
230285
</div>

‎src/browser/components/Settings/types.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export interface ProviderConfigDisplay {
1111
apiKeySet: boolean;
1212
baseUrl?: string;
1313
models?: string[];
14+
// Bedrock-specific fields
15+
region?: string;
16+
bearerTokenSet?: boolean;
17+
accessKeyIdSet?: boolean;
18+
secretAccessKeySet?: boolean;
19+
// Allow additional fields for extensibility
20+
[key: string]: unknown;
1421
}
1522

1623
export type ProvidersConfigMap = Record<string, ProviderConfigDisplay>;

0 commit comments

Comments
 (0)