Skip to content

Commit e7f28c2

Browse files
dafzthomasclaude
andcommitted
feat: add Bedrock support for Claude Agent SDK adapter
- Add useBedrock, awsRegion, awsProfile, and per-tier ARN override fields to ClaudeProviderStartOptions contract - Add resolveBedrockModel() and buildBedrockEnv() helpers to ClaudeAdapter for model slug → ARN resolution and env injection - Store bedrockOptions in session context so sendTurn can resolve models without receiving providerOptions on each turn - Wire Bedrock settings from appSettings through providerOptions dispatch in ChatView - Add Bedrock configuration section to settings UI with toggle, region, profile, and per-tier ARN override inputs - Extend desktop syncShellEnvironment to read AWS/Bedrock env vars from login shell (AWS_REGION, AWS_PROFILE, credentials, etc.) - Add tests for AWS env var sync in desktop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d74121 commit e7f28c2

7 files changed

Lines changed: 353 additions & 15 deletions

File tree

apps/desktop/src/syncShellEnvironment.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ describe("syncShellEnvironment", () => {
1818
readEnvironment,
1919
});
2020

21-
expect(readEnvironment).toHaveBeenCalledWith("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"]);
21+
expect(readEnvironment).toHaveBeenCalledWith("/bin/zsh", [
22+
"PATH",
23+
"SSH_AUTH_SOCK",
24+
"AWS_REGION",
25+
"AWS_DEFAULT_REGION",
26+
"AWS_PROFILE",
27+
"AWS_ACCESS_KEY_ID",
28+
"AWS_SECRET_ACCESS_KEY",
29+
"AWS_SESSION_TOKEN",
30+
"CLAUDE_CODE_USE_BEDROCK",
31+
]);
2232
expect(env.PATH).toBe("/opt/homebrew/bin:/usr/bin");
2333
expect(env.SSH_AUTH_SOCK).toBe("/tmp/secretive.sock");
2434
});
@@ -82,4 +92,48 @@ describe("syncShellEnvironment", () => {
8292
expect(env.PATH).toBe("/usr/bin");
8393
expect(env.SSH_AUTH_SOCK).toBe("/tmp/inherited.sock");
8494
});
95+
96+
it("hydrates missing AWS/Bedrock env vars from the login shell", () => {
97+
const env: NodeJS.ProcessEnv = {
98+
SHELL: "/bin/zsh",
99+
PATH: "/usr/bin",
100+
};
101+
const readEnvironment = vi.fn(() => ({
102+
PATH: "/usr/bin",
103+
AWS_REGION: "us-west-2",
104+
AWS_PROFILE: "bedrock-dev",
105+
CLAUDE_CODE_USE_BEDROCK: "1",
106+
}));
107+
108+
syncShellEnvironment(env, {
109+
platform: "darwin",
110+
readEnvironment,
111+
});
112+
113+
expect(env.AWS_REGION).toBe("us-west-2");
114+
expect(env.AWS_PROFILE).toBe("bedrock-dev");
115+
expect(env.CLAUDE_CODE_USE_BEDROCK).toBe("1");
116+
});
117+
118+
it("preserves inherited AWS env vars over login shell values", () => {
119+
const env: NodeJS.ProcessEnv = {
120+
SHELL: "/bin/zsh",
121+
PATH: "/usr/bin",
122+
AWS_REGION: "eu-west-1",
123+
AWS_PROFILE: "production",
124+
};
125+
const readEnvironment = vi.fn(() => ({
126+
PATH: "/usr/bin",
127+
AWS_REGION: "us-west-2",
128+
AWS_PROFILE: "bedrock-dev",
129+
}));
130+
131+
syncShellEnvironment(env, {
132+
platform: "darwin",
133+
readEnvironment,
134+
});
135+
136+
expect(env.AWS_REGION).toBe("eu-west-1");
137+
expect(env.AWS_PROFILE).toBe("production");
138+
});
85139
});

apps/desktop/src/syncShellEnvironment.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { readEnvironmentFromLoginShell, ShellEnvironmentReader } from "@t3tools/shared/shell";
22

3+
const AWS_ENV_VARS = [
4+
"AWS_REGION",
5+
"AWS_DEFAULT_REGION",
6+
"AWS_PROFILE",
7+
"AWS_ACCESS_KEY_ID",
8+
"AWS_SECRET_ACCESS_KEY",
9+
"AWS_SESSION_TOKEN",
10+
"CLAUDE_CODE_USE_BEDROCK",
11+
] as const;
12+
313
export function syncShellEnvironment(
414
env: NodeJS.ProcessEnv = process.env,
515
options: {
@@ -14,6 +24,7 @@ export function syncShellEnvironment(
1424
const shellEnvironment = (options.readEnvironment ?? readEnvironmentFromLoginShell)(shell, [
1525
"PATH",
1626
"SSH_AUTH_SOCK",
27+
...AWS_ENV_VARS,
1728
]);
1829

1930
if (shellEnvironment.PATH) {
@@ -23,6 +34,12 @@ export function syncShellEnvironment(
2334
if (!env.SSH_AUTH_SOCK && shellEnvironment.SSH_AUTH_SOCK) {
2435
env.SSH_AUTH_SOCK = shellEnvironment.SSH_AUTH_SOCK;
2536
}
37+
38+
for (const key of AWS_ENV_VARS) {
39+
if (!env[key] && shellEnvironment[key]) {
40+
env[key] = shellEnvironment[key];
41+
}
42+
}
2643
} catch {
2744
// Keep inherited environment if shell lookup fails.
2845
}

apps/server/src/provider/Layers/ClaudeAdapter.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ interface ClaudeSessionContext {
128128
readonly query: ClaudeQueryRuntime;
129129
readonly startedAt: string;
130130
readonly basePermissionMode: PermissionMode | undefined;
131+
readonly bedrockOptions: BedrockOptions | undefined;
131132
resumeSessionId: string | undefined;
132133
readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
133134
readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;
@@ -199,6 +200,71 @@ function toPermissionMode(value: unknown): PermissionMode | undefined {
199200
}
200201
}
201202

203+
// ── Bedrock helpers ──────────────────────────────────────────────────
204+
205+
interface BedrockOptions {
206+
readonly useBedrock?: boolean | undefined;
207+
readonly awsRegion?: string | undefined;
208+
readonly awsProfile?: string | undefined;
209+
readonly bedrockModelOverrideHaiku?: string | undefined;
210+
readonly bedrockModelOverrideSonnet?: string | undefined;
211+
readonly bedrockModelOverrideOpus?: string | undefined;
212+
}
213+
214+
function isBedrockActive(opts: BedrockOptions | undefined): boolean {
215+
if (!opts) return false;
216+
return Boolean(
217+
opts.useBedrock ||
218+
opts.awsRegion ||
219+
opts.awsProfile ||
220+
opts.bedrockModelOverrideHaiku ||
221+
opts.bedrockModelOverrideSonnet ||
222+
opts.bedrockModelOverrideOpus,
223+
);
224+
}
225+
226+
function resolveBedrockModel(
227+
modelSlug: string | undefined,
228+
opts: BedrockOptions,
229+
): string | undefined {
230+
if (!modelSlug) return opts.bedrockModelOverrideSonnet ?? opts.bedrockModelOverrideOpus;
231+
const slug = modelSlug.toLowerCase();
232+
if (slug.includes("haiku") && opts.bedrockModelOverrideHaiku) return opts.bedrockModelOverrideHaiku;
233+
if (slug.includes("opus") && opts.bedrockModelOverrideOpus) return opts.bedrockModelOverrideOpus;
234+
if (opts.bedrockModelOverrideSonnet) return opts.bedrockModelOverrideSonnet;
235+
if (opts.bedrockModelOverrideOpus) return opts.bedrockModelOverrideOpus;
236+
return undefined;
237+
}
238+
239+
function buildBedrockEnv(
240+
baseEnv: NodeJS.ProcessEnv,
241+
opts: BedrockOptions,
242+
resolvedModelArn: string | undefined,
243+
): NodeJS.ProcessEnv {
244+
const env: NodeJS.ProcessEnv = { ...baseEnv };
245+
env.CLAUDE_CODE_USE_BEDROCK = "1";
246+
if (opts.awsRegion) {
247+
env.AWS_REGION = opts.awsRegion;
248+
env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION = opts.awsRegion;
249+
}
250+
if (opts.awsProfile) {
251+
env.AWS_PROFILE = opts.awsProfile;
252+
}
253+
if (opts.bedrockModelOverrideHaiku) {
254+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = opts.bedrockModelOverrideHaiku;
255+
}
256+
if (opts.bedrockModelOverrideSonnet) {
257+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = opts.bedrockModelOverrideSonnet;
258+
}
259+
if (opts.bedrockModelOverrideOpus) {
260+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = opts.bedrockModelOverrideOpus;
261+
}
262+
if (resolvedModelArn) {
263+
env.ANTHROPIC_MODEL = resolvedModelArn;
264+
}
265+
return env;
266+
}
267+
202268
function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined {
203269
if (!resumeCursor || typeof resumeCursor !== "object") {
204270
return undefined;
@@ -2209,6 +2275,14 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
22092275
);
22102276

22112277
const providerOptions = input.providerOptions?.claudeAgent;
2278+
const bedrock = isBedrockActive(providerOptions);
2279+
const resolvedModelArn = bedrock
2280+
? resolveBedrockModel(input.model, providerOptions!)
2281+
: undefined;
2282+
const sessionModel = bedrock ? resolvedModelArn : input.model;
2283+
const sessionEnv = bedrock
2284+
? buildBedrockEnv(process.env, providerOptions!, resolvedModelArn)
2285+
: process.env;
22122286
const requestedEffort = resolveReasoningEffortForProvider(
22132287
"claudeAgent",
22142288
input.modelOptions?.claudeAgent?.effort ?? null,
@@ -2236,7 +2310,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
22362310

22372311
const queryOptions: ClaudeQueryOptions = {
22382312
...(input.cwd ? { cwd: input.cwd } : {}),
2239-
...(input.model ? { model: input.model } : {}),
2313+
...(sessionModel ? { model: sessionModel } : {}),
22402314
...(providerOptions?.binaryPath
22412315
? { pathToClaudeCodeExecutable: providerOptions.binaryPath }
22422316
: {}),
@@ -2253,7 +2327,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
22532327
...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}),
22542328
includePartialMessages: true,
22552329
canUseTool,
2256-
env: process.env,
2330+
env: sessionEnv,
22572331
...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
22582332
};
22592333

@@ -2298,6 +2372,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
22982372
query: queryRuntime,
22992373
startedAt,
23002374
basePermissionMode: permissionMode,
2375+
bedrockOptions: bedrock ? providerOptions : undefined,
23012376
resumeSessionId: resumeState?.resume,
23022377
pendingApprovals,
23032378
pendingUserInputs,
@@ -2375,10 +2450,15 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) {
23752450
}
23762451

23772452
if (input.model) {
2378-
yield* Effect.tryPromise({
2379-
try: () => context.query.setModel(input.model),
2380-
catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),
2381-
});
2453+
const turnModel = context.bedrockOptions
2454+
? resolveBedrockModel(input.model, context.bedrockOptions)
2455+
: input.model;
2456+
if (turnModel) {
2457+
yield* Effect.tryPromise({
2458+
try: () => context.query.setModel(turnModel),
2459+
catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause),
2460+
});
2461+
}
23822462
}
23832463

23842464
// Apply interaction mode by switching the SDK's permission mode.

apps/web/src/appSettings.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ const AppSettingsSchema = Schema.Struct({
3939
Schema.withConstructorDefault(() => Option.some([])),
4040
),
4141
textGenerationModel: Schema.optional(TrimmedNonEmptyString),
42+
claudeUseBedrock: Schema.Boolean.pipe(
43+
Schema.withConstructorDefault(() => Option.some(false)),
44+
),
45+
claudeAwsRegion: Schema.String.check(Schema.isMaxLength(256)).pipe(
46+
Schema.withConstructorDefault(() => Option.some("")),
47+
),
48+
claudeAwsProfile: Schema.String.check(Schema.isMaxLength(256)).pipe(
49+
Schema.withConstructorDefault(() => Option.some("")),
50+
),
51+
claudeBedrockArnHaiku: Schema.String.check(Schema.isMaxLength(1024)).pipe(
52+
Schema.withConstructorDefault(() => Option.some("")),
53+
),
54+
claudeBedrockArnSonnet: Schema.String.check(Schema.isMaxLength(1024)).pipe(
55+
Schema.withConstructorDefault(() => Option.some("")),
56+
),
57+
claudeBedrockArnOpus: Schema.String.check(Schema.isMaxLength(1024)).pipe(
58+
Schema.withConstructorDefault(() => Option.some("")),
59+
),
4260
});
4361
export type AppSettings = typeof AppSettingsSchema.Type;
4462
export interface AppModelOption {

apps/web/src/components/ChatView.tsx

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -661,16 +661,50 @@ export default function ChatView({ threadId }: ChatViewProps) {
661661
return undefined;
662662
}, [draftModelOptions, selectedModel, selectedProvider]);
663663
const providerOptionsForDispatch = useMemo(() => {
664-
if (!settings.codexBinaryPath && !settings.codexHomePath) {
665-
return undefined;
666-
}
664+
const codex =
665+
settings.codexBinaryPath || settings.codexHomePath
666+
? {
667+
...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}),
668+
...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}),
669+
}
670+
: undefined;
671+
const claudeAgent =
672+
settings.claudeUseBedrock ||
673+
settings.claudeAwsRegion ||
674+
settings.claudeAwsProfile ||
675+
settings.claudeBedrockArnHaiku ||
676+
settings.claudeBedrockArnSonnet ||
677+
settings.claudeBedrockArnOpus
678+
? {
679+
...(settings.claudeUseBedrock ? { useBedrock: true } : {}),
680+
...(settings.claudeAwsRegion ? { awsRegion: settings.claudeAwsRegion } : {}),
681+
...(settings.claudeAwsProfile ? { awsProfile: settings.claudeAwsProfile } : {}),
682+
...(settings.claudeBedrockArnHaiku
683+
? { bedrockModelOverrideHaiku: settings.claudeBedrockArnHaiku }
684+
: {}),
685+
...(settings.claudeBedrockArnSonnet
686+
? { bedrockModelOverrideSonnet: settings.claudeBedrockArnSonnet }
687+
: {}),
688+
...(settings.claudeBedrockArnOpus
689+
? { bedrockModelOverrideOpus: settings.claudeBedrockArnOpus }
690+
: {}),
691+
}
692+
: undefined;
693+
if (!codex && !claudeAgent) return undefined;
667694
return {
668-
codex: {
669-
...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}),
670-
...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}),
671-
},
695+
...(codex ? { codex } : {}),
696+
...(claudeAgent ? { claudeAgent } : {}),
672697
};
673-
}, [settings.codexBinaryPath, settings.codexHomePath]);
698+
}, [
699+
settings.codexBinaryPath,
700+
settings.codexHomePath,
701+
settings.claudeUseBedrock,
702+
settings.claudeAwsRegion,
703+
settings.claudeAwsProfile,
704+
settings.claudeBedrockArnHaiku,
705+
settings.claudeBedrockArnSonnet,
706+
settings.claudeBedrockArnOpus,
707+
]);
674708
const selectedModelForPicker = selectedModel;
675709
const modelOptionsByProvider = useMemo(
676710
() => getCustomModelOptionsByProvider(settings),

0 commit comments

Comments
 (0)