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
25 changes: 25 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 51 additions & 5 deletions server/claude-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { CLAUDE_MODELS } from '../shared/modelConstants.js';
import { isTruthyValue, loadClaudeSettingsEnv } from './utils/env-helpers.js';
import {
createNotificationEvent,
notifyRunFailed,
Expand Down Expand Up @@ -141,7 +142,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, images } = options;

const sdkOptions = {};
Expand Down Expand Up @@ -190,9 +191,10 @@ 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_MODELS.DEFAULT;
// Map model (default to sonnet). In Bedrock mode this resolves model aliases
// (sonnet/opus/haiku) to configured inference profile IDs when available.
const settingsEnv = await loadClaudeSettingsEnv();
sdkOptions.model = resolveClaudeModel(options.model, settingsEnv);
console.log(`Using model: ${sdkOptions.model}`);

// Map system prompt configuration
Expand Down Expand Up @@ -459,6 +461,50 @@ 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_MODELS.BEDROCK
* 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_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_MODELS.BEDROCK));
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;
}

// Look up in centralized Bedrock constants
return CLAUDE_MODELS.BEDROCK[requestedModel] || requestedModel;
}

/**
* Executes a Claude query using the SDK
* @param {string} command - User prompt/command
Expand All @@ -483,7 +529,7 @@ async function queryClaudeSDK(command, options = {}, ws) {

try {
// Map CLI options to SDK format
const sdkOptions = mapCliOptionsToSDK(options);
const sdkOptions = await mapCliOptionsToSDK(options);

// Load MCP configuration
const mcpServers = await loadMcpConfig(options.cwd);
Expand Down
56 changes: 31 additions & 25 deletions server/routes/cli-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { spawn } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { isTruthyValue, loadClaudeSettingsEnv } from '../utils/env-helpers.js';

const router = express.Router();

Expand All @@ -14,14 +15,16 @@ router.get('/claude/status', async (req, res) => {
return res.json({
authenticated: true,
email: credentialsResult.email || 'Authenticated',
method: credentialsResult.method // 'api_key' or 'credentials_file'
method: credentialsResult.method, // 'api_key', 'credentials_file', or 'bedrock'
isBedrock: Boolean(credentialsResult.isBedrock)
});
}

return res.json({
authenticated: false,
email: null,
method: null,
isBedrock: false,
error: credentialsResult.error || 'Not authenticated'
});

Expand All @@ -31,6 +34,7 @@ router.get('/claude/status', async (req, res) => {
authenticated: false,
email: null,
method: null,
isBedrock: false,
error: error.message
});
}
Expand Down Expand Up @@ -96,22 +100,6 @@ router.get('/gemini/status', async (req, res) => {
}
});

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 (error) {
// Ignore missing or malformed settings and fall back to other auth sources.
}

return {};
}

/**
* Checks Claude authentication credentials using two methods with priority order:
*
Expand All @@ -134,35 +122,50 @@ async function loadClaudeSettingsEnv() {
* - method: 'api_key' for env var, 'credentials_file' for OAuth tokens
*/
async function checkClaudeCredentials() {
const settingsEnv = await loadClaudeSettingsEnv();

// Priority 0: Bedrock mode bypasses Claude OAuth checks.
const bedrockEnabled = isTruthyValue(process.env.CLAUDE_CODE_USE_BEDROCK)
|| isTruthyValue(settingsEnv.CLAUDE_CODE_USE_BEDROCK);
if (bedrockEnabled) {
return {
authenticated: true,
email: 'AWS Bedrock',
method: 'bedrock',
isBedrock: true
};
}

// Priority 1: Check for ANTHROPIC_API_KEY environment variable
// The SDK checks this first and uses it if present, even if OAuth tokens exist.
// When set, API calls are charged via pay-as-you-go rates instead of subscription.
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
return {
authenticated: true,
email: 'API Key Auth',
method: 'api_key'
method: 'api_key',
isBedrock: false
};
}

// Priority 1b: Check ~/.claude/settings.json env values.
// Claude Code can read proxy/auth values from settings.json even when the
// CloudCLI server process itself was not started with those env vars exported.
const settingsEnv = await loadClaudeSettingsEnv();

if (typeof settingsEnv.ANTHROPIC_API_KEY === 'string' && settingsEnv.ANTHROPIC_API_KEY.trim()) {
return {
authenticated: true,
email: 'API Key Auth',
method: 'api_key'
method: 'api_key',
isBedrock: false
};
}

if (typeof settingsEnv.ANTHROPIC_AUTH_TOKEN === 'string' && settingsEnv.ANTHROPIC_AUTH_TOKEN.trim()) {
return {
authenticated: true,
email: 'Configured via settings.json',
method: 'api_key'
method: 'api_key',
isBedrock: false
};
}

Expand All @@ -182,21 +185,24 @@ async function checkClaudeCredentials() {
return {
authenticated: true,
email: creds.email || creds.user || null,
method: 'credentials_file'
method: 'credentials_file',
isBedrock: false
};
}
}

return {
authenticated: false,
email: null,
method: null
method: null,
isBedrock: false
};
} catch (error) {
return {
authenticated: false,
email: null,
method: null
method: null,
isBedrock: false
};
}
}
Expand Down
39 changes: 39 additions & 0 deletions server/utils/env-helpers.js
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>>} 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 {};
}
18 changes: 18 additions & 0 deletions shared/modelConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ export const CLAUDE_MODELS = {
],

DEFAULT: "sonnet",

/**
* AWS Bedrock model IDs keyed by SDK alias.
*
* The Claude Agent SDK supports both `global.anthropic.*` (dynamic
* routing, no pricing premium) and `anthropic.*` (base) model IDs.
* Regional prefixes (`us.`, `eu.`, etc.) are also valid but carry a
* 10% premium and are only needed for data-residency requirements.
*
* @see https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock
*/
BEDROCK: {
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",
},
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const DEFAULT_AUTH_STATUS: AuthStatus = {
email: null,
loading: true,
error: null,
isBedrock: false,
};

export const DEFAULT_MCP_TEST_RESULT: McpTestResult = {
Expand Down
4 changes: 4 additions & 0 deletions src/components/settings/hooks/useSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type StatusApiResponse = {
email?: string | null;
error?: string | null;
method?: string;
isBedrock?: boolean;
};

type JsonResult = {
Expand Down Expand Up @@ -278,6 +279,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
email: null,
loading: false,
error: 'Failed to check authentication status',
isBedrock: false,
});
return;
}
Expand All @@ -289,6 +291,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
loading: false,
error: data.error || null,
method: data.method,
isBedrock: Boolean(data.isBedrock),
});
} catch (error) {
console.error(`Error checking ${provider} auth status:`, error);
Expand All @@ -297,6 +300,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
email: null,
loading: false,
error: getErrorMessage(error),
isBedrock: false,
});
}
}, [setAuthStatusByProvider]);
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type AuthStatus = {
loading: boolean;
error: string | null;
method?: string;
isBedrock?: boolean;
};

export type KeyValueMap = Record<string, string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
</div>
</div>

{authStatus.method !== 'api_key' && (
{authStatus.method !== 'api_key' && !authStatus.isBedrock && (
<div className="border-t border-border/50 pt-4">
<div className="flex items-center justify-between">
<div>
Expand Down