diff --git a/.env.example b/.env.example index 7e1d124c72..a41763f42e 100755 --- a/.env.example +++ b/.env.example @@ -43,3 +43,28 @@ VITE_CONTEXT_WINDOW=160000 CONTEXT_WINDOW=160000 +# ============================================================================= +# AWS BEDROCK CONFIGURATION FOR CLAUDE +# ============================================================================= +# +# Prerequisites: +# - AWS CLI must be installed and configured +# - Run 'aws configure' to set up credentials and default region +# - Or provide AWS credentials through environment variables or IAM roles +# +# To use AWS Bedrock instead of the Anthropic API, uncomment the lines below. +# CLAUDE_CODE_USE_BEDROCK=1 +# +# AWS authentication for Bedrock: +# - Prefer IAM role or AWS_PROFILE from ~/.aws/config and ~/.aws/credentials +# - Optionally use AWS_BEARER_TOKEN_BEDROCK for Bedrock API key auth +# AWS_REGION=eu-central-1 +# AWS_PROFILE=default +# AWS_BEARER_TOKEN_BEDROCK=your-bedrock-bearer-token +# +# Model defaults for Bedrock are defined in shared/modelConstants.js. +# The UI aliases (sonnet/opus/haiku) automatically resolve to the correct +# Bedrock model IDs (anthropic.claude-*). Override only if you need a +# custom inference profile or a specific regional endpoint: +# ANTHROPIC_MODEL=us.anthropic.claude-sonnet-4-6 + diff --git a/server/claude-sdk.js b/server/claude-sdk.js index 5d8a27d717..b18902ecba 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -17,7 +17,11 @@ import crypto from 'crypto'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; -import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js'; +import { + CLAUDE_BEDROCK_MODELS, + CLAUDE_FALLBACK_MODELS, +} from './modules/providers/list/claude/claude-models.provider.js'; +import { isTruthyValue, loadClaudeSettingsEnv } from './utils/env-helpers.js'; import { providerModelsService } from './modules/providers/services/provider-models.service.js'; import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js'; import { @@ -146,7 +150,7 @@ function matchesToolPermission(entry, toolName, input) { * @param {Object} options - CLI options * @returns {Object} SDK-compatible options */ -function mapCliOptionsToSDK(options = {}) { +async function mapCliOptionsToSDK(options = {}) { const { sessionId, cwd, toolsSettings, permissionMode } = options; const sdkOptions = {}; @@ -203,10 +207,9 @@ function mapCliOptionsToSDK(options = {}) { sdkOptions.disallowedTools = settings.disallowedTools || []; - // Map model (default to sonnet) - // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m] - sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT; - // Model logged at query start below + // Map model. In Bedrock mode this resolves UI aliases to model IDs. + const settingsEnv = await loadClaudeSettingsEnv(); + sdkOptions.model = resolveClaudeModel(options.model, settingsEnv); // Map system prompt configuration sdkOptions.systemPrompt = { @@ -494,6 +497,49 @@ async function loadMcpConfig(cwd) { } } +function resolveClaudeEnvValue(key, settingsEnv) { + const processValue = process.env[key]; + if (typeof processValue === 'string' && processValue.trim()) { + return processValue.trim(); + } + + const settingsValue = settingsEnv[key]; + if (typeof settingsValue === 'string' && settingsValue.trim()) { + return settingsValue.trim(); + } + + return ''; +} + +/** + * Resolves a UI model alias (e.g. "sonnet") to the actual model ID. + * + * When Bedrock is enabled, looks up the alias in CLAUDE_BEDROCK_MODELS + * for sensible defaults. Users can still override via ANTHROPIC_MODEL + * (in env or ~/.claude/settings.json) for custom inference profiles. + */ +function resolveClaudeModel(modelAlias, settingsEnv) { + const requestedModel = modelAlias || CLAUDE_FALLBACK_MODELS.DEFAULT; + const isBedrockEnabled = isTruthyValue(resolveClaudeEnvValue('CLAUDE_CODE_USE_BEDROCK', settingsEnv)); + if (!isBedrockEnabled) { + return requestedModel; + } + + // If the caller passed a specific model ID (not a UI alias), honour it directly + const UI_ALIASES = new Set(Object.keys(CLAUDE_BEDROCK_MODELS)); + if (modelAlias && !UI_ALIASES.has(requestedModel)) { + return requestedModel; + } + + // Allow explicit env override for custom inference profiles / regions + const explicitModel = resolveClaudeEnvValue('ANTHROPIC_MODEL', settingsEnv); + if (explicitModel) { + return explicitModel; + } + + return CLAUDE_BEDROCK_MODELS[requestedModel] || requestedModel; +} + /** * Executes a Claude query using the SDK * @param {string} command - User prompt/command @@ -524,7 +570,7 @@ async function queryClaudeSDK(command, options = {}, ws) { ); // Map CLI options to SDK format - const sdkOptions = mapCliOptionsToSDK({ + const sdkOptions = await mapCliOptionsToSDK({ ...options, model: resolvedModel || options.model, }); diff --git a/server/modules/providers/list/claude/claude-auth.provider.ts b/server/modules/providers/list/claude/claude-auth.provider.ts index 68874b9dfc..0a95e90559 100644 --- a/server/modules/providers/list/claude/claude-auth.provider.ts +++ b/server/modules/providers/list/claude/claude-auth.provider.ts @@ -13,9 +13,19 @@ type ClaudeCredentialsStatus = { authenticated: boolean; email: string | null; method: string | null; + isBedrock?: boolean; error?: string; }; +const isTruthyValue = (value: unknown): boolean => { + if (typeof value !== 'string') { + return false; + } + + const normalized = value.trim().toLowerCase(); + return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on'; +}; + const hasErrorCode = (error: unknown, code: string): boolean => ( error instanceof Error && 'code' in error && error.code === code ); @@ -59,6 +69,7 @@ export class ClaudeProviderAuth implements IProviderAuth { authenticated: credentials.authenticated, email: credentials.authenticated ? credentials.email || 'Authenticated' : credentials.email, method: credentials.method, + isBedrock: Boolean(credentials.isBedrock), error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated', }; } @@ -88,6 +99,17 @@ export class ClaudeProviderAuth implements IProviderAuth { } const settingsEnv = await this.loadSettingsEnv(); + const bedrockEnabled = isTruthyValue(process.env.CLAUDE_CODE_USE_BEDROCK) + || isTruthyValue(readOptionalString(settingsEnv.CLAUDE_CODE_USE_BEDROCK)); + if (bedrockEnabled) { + return { + authenticated: true, + email: 'AWS Bedrock', + method: 'bedrock', + isBedrock: true, + }; + } + if (readOptionalString(settingsEnv.ANTHROPIC_API_KEY)) { return { authenticated: true, email: 'API Key Auth', method: 'api_key' }; } diff --git a/server/modules/providers/list/claude/claude-models.provider.ts b/server/modules/providers/list/claude/claude-models.provider.ts index 81e89027f3..4ce7436492 100644 --- a/server/modules/providers/list/claude/claude-models.provider.ts +++ b/server/modules/providers/list/claude/claude-models.provider.ts @@ -38,6 +38,16 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = { ], DEFAULT: 'default', }; + +export const CLAUDE_BEDROCK_MODELS: Record = { + default: 'anthropic.claude-sonnet-4-6', + sonnet: 'anthropic.claude-sonnet-4-6', + 'sonnet[1m]': 'anthropic.claude-sonnet-4-6', + opus: 'anthropic.claude-opus-4-6-v1', + opusplan: 'anthropic.claude-opus-4-6-v1', + haiku: 'anthropic.claude-haiku-4-5-20251001-v1:0', +}; + type ClaudeInitEvent = { sessionId?: string; session_id?: string; diff --git a/server/shared/types.ts b/server/shared/types.ts index dbcef5d427..1af5f5dd23 100644 --- a/server/shared/types.ts +++ b/server/shared/types.ts @@ -408,6 +408,7 @@ export type ProviderAuthStatus = { authenticated: boolean; email: string | null; method: string | null; + isBedrock?: boolean; error?: string; }; diff --git a/server/utils/env-helpers.js b/server/utils/env-helpers.js new file mode 100644 index 0000000000..fa32e0dde8 --- /dev/null +++ b/server/utils/env-helpers.js @@ -0,0 +1,39 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; + +/** + * Returns true if the value is a string considered "truthy" for env flags + * (e.g. CLAUDE_CODE_USE_BEDROCK). Accepts '1', 'true', 'yes', 'on' (case-insensitive). + * @param {unknown} value + * @returns {boolean} + */ +export function isTruthyValue(value) { + if (typeof value !== 'string') { + return false; + } + + const normalized = value.trim().toLowerCase(); + return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on'; +} + +/** + * Loads env key/value pairs from ~/.claude/settings.json (settings.env). + * Used for auth and model config that may be set in Claude Code settings. + * @returns {Promise>} Env object or {} on missing/malformed file. + */ +export async function loadClaudeSettingsEnv() { + try { + const settingsPath = path.join(os.homedir(), '.claude', 'settings.json'); + const content = await fs.readFile(settingsPath, 'utf8'); + const settings = JSON.parse(content); + + if (settings?.env && typeof settings.env === 'object') { + return settings.env; + } + } catch { + // Ignore missing or malformed settings and fall back to empty. + } + + return {}; +} diff --git a/src/components/provider-auth/hooks/useProviderAuthStatus.ts b/src/components/provider-auth/hooks/useProviderAuthStatus.ts index 9231e7701a..ab8ff273ae 100644 --- a/src/components/provider-auth/hooks/useProviderAuthStatus.ts +++ b/src/components/provider-auth/hooks/useProviderAuthStatus.ts @@ -15,6 +15,7 @@ type ProviderAuthStatusPayload = { authenticated?: boolean; email?: string | null; method?: string | null; + isBedrock?: boolean; error?: string | null; }; @@ -37,6 +38,7 @@ const toProviderAuthStatus = ( authenticated: Boolean(payload.authenticated), email: payload.email ?? null, method: payload.method ?? null, + isBedrock: Boolean(payload.isBedrock), error: payload.error ?? fallbackError, loading: false, }); diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts index afa0809411..ce0376b74a 100644 --- a/src/components/provider-auth/types.ts +++ b/src/components/provider-auth/types.ts @@ -4,6 +4,7 @@ export type ProviderAuthStatus = { authenticated: boolean; email: string | null; method: string | null; + isBedrock?: boolean; error: string | null; loading: boolean; }; diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx index c0ed69d5a0..1095a78939 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx @@ -119,7 +119,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo - {authStatus.method !== 'api_key' && ( + {authStatus.method !== 'api_key' && !authStatus.isBedrock && (