diff --git a/AGENTS.md b/AGENTS.md index ce4dc90..845abed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,7 @@ Default `` is `~/.local/bin` on macOS/Linux and `~/.cc-mirror/bin` on W | openrouter | Auth Token | `ANTHROPIC_AUTH_TOKEN` | | ccrouter | Optional | placeholder token | | mirror | None | user authenticates normally | +| bedrock | None | AWS SDK credential chain | ### Model Mapping (env vars) @@ -200,8 +201,54 @@ export const MINIMAX_BLOCKED_TOOLS = [ ]; ``` +**bedrock blocked tools:** None (pure Claude experience) + **Team mode merging**: When enabled, `configureTeamToolset` merges provider's blocked tools with `['TodoWrite']`. +## Amazon Bedrock Provider + +Bedrock uses Claude Code's native AWS SDK integration. Key configuration: + +### Environment Variables + +```bash +# Required - enables Bedrock mode +CLAUDE_CODE_USE_BEDROCK=1 + +# Default region (overridable) +AWS_REGION=us-east-1 + +# Model IDs - user must provide (requiresModelMapping: true) +ANTHROPIC_DEFAULT_SONNET_MODEL=us.anthropic.claude-sonnet-4-5-20250929-v1:0 +ANTHROPIC_DEFAULT_OPUS_MODEL=us.anthropic.claude-opus-4-5-20251101-v1:0 +ANTHROPIC_DEFAULT_HAIKU_MODEL=us.anthropic.claude-haiku-4-5-20251001-v1:0 +``` + +### Authentication (handled by AWS SDK) + +1. `AWS_BEARER_TOKEN_BEDROCK` - Bearer token (highest priority) +2. `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` - Explicit credentials +3. `AWS_SESSION_TOKEN` - For temporary credentials +4. `AWS_PROFILE` - AWS profile name +5. Default credential chain (`~/.aws/credentials`, IAM roles) + +### Model ID Formats + +| Format | Example | Use Case | +| ------ | ------- | -------- | +| Regional (`us.`) | `us.anthropic.claude-sonnet-4-5-20250929-v1:0` | Routes to specific region | +| Global (`global.`) | `global.anthropic.claude-sonnet-4-5-20250929-v1:0` | Cross-region routing | +| Inference Profile ARN | `arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile` | Custom profiles | + +### Create Example + +```bash +npx cc-mirror create --provider bedrock --name br \ + --model-sonnet "us.anthropic.claude-sonnet-4-5-20250929-v1:0" \ + --model-opus "us.anthropic.claude-opus-4-5-20251101-v1:0" \ + --model-haiku "us.anthropic.claude-haiku-4-5-20251001-v1:0" +``` + ## Prompt Pack - Only `minimal` mode supported (maximal deprecated) diff --git a/README.md b/README.md index b82dadf..df14d2c 100644 --- a/README.md +++ b/README.md @@ -140,12 +140,13 @@ npx cc-mirror quick --provider mirror --name mclaude Want to use different models? CC-MIRROR supports multiple providers: -| Provider | Models | Auth | Best For | -| -------------- | ---------------------- | ---------- | ------------------------------- | -| **Z.ai** | GLM-4.7, GLM-4.5-Air | API Key | Heavy coding with GLM reasoning | -| **MiniMax** | MiniMax-M2.1 | API Key | Unified model experience | -| **OpenRouter** | 100+ models | Auth Token | Model flexibility, pay-per-use | -| **CCRouter** | Ollama, DeepSeek, etc. | Optional | Local-first development | +| Provider | Models | Auth | Best For | +| -------------- | ---------------------- | ------------ | ------------------------------- | +| **Z.ai** | GLM-4.7, GLM-4.5-Air | API Key | Heavy coding with GLM reasoning | +| **MiniMax** | MiniMax-M2.1 | API Key | Unified model experience | +| **OpenRouter** | 100+ models | Auth Token | Model flexibility, pay-per-use | +| **CCRouter** | Ollama, DeepSeek, etc. | Optional | Local-first development | +| **Bedrock** | Claude via AWS | AWS SDK | Enterprise AWS integration | ```bash # Z.ai (GLM Coding Plan) @@ -160,6 +161,40 @@ npx cc-mirror quick --provider openrouter --api-key "$OPENROUTER_API_KEY" \ # Claude Code Router (local LLMs) npx cc-mirror quick --provider ccrouter + +# Amazon Bedrock (Claude via AWS) +npx cc-mirror quick --provider bedrock --name br \ + --model-sonnet "us.anthropic.claude-sonnet-4-5-20250929-v1:0" \ + --model-opus "us.anthropic.claude-opus-4-5-20251101-v1:0" \ + --model-haiku "us.anthropic.claude-haiku-4-5-20251001-v1:0" +``` + +### Amazon Bedrock + +Bedrock uses native AWS SDK authentication. No API key is needed — configure AWS credentials via: +- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) +- AWS profile (`AWS_PROFILE`) +- IAM role (EC2/ECS/Lambda) +- Bearer token (`AWS_BEARER_TOKEN_BEDROCK`) + +Model IDs use Bedrock format. Use `us.` prefix for regional or `global.` for cross-region routing: + +```bash +# Regional (routes to specific region) +npx cc-mirror quick --provider bedrock --name br-regional \ + --model-sonnet "us.anthropic.claude-sonnet-4-5-20250929-v1:0" \ + --model-opus "us.anthropic.claude-opus-4-5-20251101-v1:0" \ + --model-haiku "us.anthropic.claude-haiku-4-5-20251001-v1:0" + +# Global (cross-region routing for higher availability) +npx cc-mirror quick --provider bedrock --name br-global \ + --model-sonnet "global.anthropic.claude-sonnet-4-5-20250929-v1:0" \ + --model-opus "global.anthropic.claude-opus-4-5-20251101-v1:0" \ + --model-haiku "global.anthropic.claude-haiku-4-5-20251001-v1:0" + +# Custom inference profile ARN +npx cc-mirror quick --provider bedrock --name br-profile \ + --model-sonnet "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile" ``` --- @@ -298,14 +333,14 @@ minimax # Run MiniMax variant ## CLI Options ``` ---provider mirror | zai | minimax | openrouter | ccrouter | custom +--provider mirror | zai | minimax | openrouter | ccrouter | bedrock | custom --name Variant name (becomes the CLI command) --api-key Provider API key --base-url Custom API endpoint --model-sonnet Map to sonnet model --model-opus Map to opus model --model-haiku Map to haiku model ---brand Theme: auto | zai | minimax | openrouter | ccrouter | mirror +--brand Theme: auto | zai | minimax | openrouter | ccrouter | mirror | bedrock --no-tweak Skip tweakcc theme --no-prompt-pack Skip provider prompt pack --verbose Show full tweakcc output during update @@ -324,6 +359,7 @@ Each provider includes a custom color theme via [tweakcc](https://github.com/Pie | **minimax** | Coral/red/orange spectrum | | **openrouter** | Teal/cyan gradient | | **ccrouter** | Sky blue accents | +| **bedrock** | AWS orange/amber with navy | --- diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index c2659e0..6ac02c8 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -68,7 +68,7 @@ src/ │ └── prompt-pack/ # System prompt overlays │ ├── providers/ # Provider templates -│ └── index.ts # zai, minimax, openrouter, ccrouter, mirror +│ └── index.ts # zai, minimax, openrouter, ccrouter, mirror, bedrock │ └── brands/ # Theme presets ├── index.ts # Brand registry @@ -76,7 +76,8 @@ src/ ├── minimax.ts # Coral theme ├── openrouter.ts # Teal theme ├── ccrouter.ts # Sky theme - └── mirror.ts # Silver/chrome theme + ├── mirror.ts # Silver/chrome theme + └── bedrock.ts # AWS orange/amber theme ``` --- @@ -217,20 +218,22 @@ interface ProviderTemplate { ### Provider Comparison ``` -┌───────────────┬────────────────────────────────────────────────────────────┐ -│ │ Provider Types │ -│ ├────────────┬────────────┬────────────┬────────────────────┤ -│ Feature │ Proxy │ Router │ Direct │ Description │ -│ │ (zai, │ (ccrouter) │ (mirror) │ │ -│ │ minimax, │ │ │ │ -│ │ openrouter)│ │ │ │ -├───────────────┼────────────┼────────────┼────────────┼────────────────────┤ -│ BASE_URL │ ✓ Set │ ✓ Set │ ✗ Not set │ API endpoint │ -│ API_KEY │ ✓ Set │ Optional │ ✗ Not set │ Auth credential │ -│ Model mapping │ Auto/Req │ Handled │ ✗ Not set │ Sonnet/Opus/Haiku │ -│ Prompt pack │ Optional │ ✗ │ ✗ │ System overlays │ -│ Team mode │ Optional │ Optional │ ✓ Default │ Task tools │ -└───────────────┴────────────┴────────────┴────────────┴────────────────────┘ +┌───────────────┬──────────────────────────────────────────────────────────────────────┐ +│ │ Provider Types │ +│ ├────────────┬────────────┬────────────┬────────────┬──────────────────┤ +│ Feature │ Proxy │ Router │ Direct │ Bedrock │ Description │ +│ │ (zai, │ (ccrouter) │ (mirror) │ │ │ +│ │ minimax, │ │ │ │ │ +│ │ openrouter)│ │ │ │ │ +├───────────────┼────────────┼────────────┼────────────┼────────────┼──────────────────┤ +│ BASE_URL │ ✓ Set │ ✓ Set │ ✗ Not set │ ✗ Not set │ API endpoint │ +│ API_KEY │ ✓ Set │ Optional │ ✗ Not set │ ✗ Not set │ Auth credential │ +│ Model mapping │ Auto/Req │ Handled │ ✗ Not set │ ✓ Required │ Sonnet/Opus/Haiku│ +│ Prompt pack │ Optional │ ✗ │ ✗ │ ✗ │ System overlays │ +│ Team mode │ Optional │ Optional │ ✓ Default │ Optional │ Task tools │ +│ Special env │ ✗ │ ✗ │ ✗ │ AWS SDK │ CLAUDE_CODE_USE_ │ +│ │ │ │ │ │ BEDROCK=1 │ +└───────────────┴────────────┴────────────┴────────────┴────────────┴──────────────────┘ ``` --- diff --git a/src/brands/bedrock.ts b/src/brands/bedrock.ts new file mode 100644 index 0000000..068c2b4 --- /dev/null +++ b/src/brands/bedrock.ts @@ -0,0 +1,215 @@ +/** + * Amazon Bedrock Brand Preset + * + * AWS-themed aesthetic with orange/amber colors and dark navy accents. + * Theme concept: cloud infrastructure with warm AWS orange highlights. + */ + +import type { TweakccConfig, Theme } from './types.js'; +import { DEFAULT_THEMES } from './defaultThemes.js'; +import { formatUserMessage, getUserLabel } from './userLabel.js'; + +type Rgb = { r: number; g: number; b: number }; + +const clamp = (value: number) => Math.max(0, Math.min(255, Math.round(value))); + +const hexToRgb = (hex: string): Rgb => { + const normalized = hex.replace('#', '').trim(); + if (normalized.length === 3) { + const [r, g, b] = normalized.split(''); + return { + r: clamp(parseInt(r + r, 16)), + g: clamp(parseInt(g + g, 16)), + b: clamp(parseInt(b + b, 16)), + }; + } + if (normalized.length !== 6) { + throw new Error(`Unsupported hex color: ${hex}`); + } + return { + r: clamp(parseInt(normalized.slice(0, 2), 16)), + g: clamp(parseInt(normalized.slice(2, 4), 16)), + b: clamp(parseInt(normalized.slice(4, 6), 16)), + }; +}; + +const rgb = (hex: string) => { + const { r, g, b } = hexToRgb(hex); + return `rgb(${r},${g},${b})`; +}; + +const mix = (hexA: string, hexB: string, weight: number) => { + const a = hexToRgb(hexA); + const b = hexToRgb(hexB); + const w = Math.max(0, Math.min(1, weight)); + return `rgb(${clamp(a.r + (b.r - a.r) * w)},${clamp(a.g + (b.g - a.g) * w)},${clamp(a.b + (b.b - a.b) * w)})`; +}; + +const lighten = (hex: string, weight: number) => mix(hex, '#ffffff', weight); + +// AWS palette: Orange primary, dark navy surfaces +const palette = { + // Base surfaces - AWS dark navy + base: '#0f1b2a', + surface: '#172133', + panel: '#1e2a3d', + elevated: '#273548', + // Borders + border: '#3a4a5e', + borderStrong: '#4a5a6e', + borderGlow: '#5a6a7e', + // Text + text: '#f0f4f8', + textMuted: '#c0c8d2', + textDim: '#8090a0', + // Primary: AWS Orange + orange: '#ff9900', + orangeSoft: '#ffb84d', + orangeDeep: '#cc7a00', + // Secondary: AWS Teal/Cyan + teal: '#00a1c9', + tealSoft: '#4dc3dd', + tealDeep: '#007d9c', + // Accent: Warm amber + amber: '#ffc107', + amberSoft: '#ffd54f', + // Semantic + green: '#2ecc71', + red: '#e74c3c', + purple: '#9b59b6', +}; + +const theme: Theme = { + name: 'Bedrock AWS', + id: 'bedrock-aws', + colors: { + autoAccept: rgb(palette.green), + bashBorder: rgb(palette.orange), + claude: rgb(palette.orange), + claudeShimmer: rgb(palette.orangeSoft), + claudeBlue_FOR_SYSTEM_SPINNER: rgb(palette.teal), + claudeBlueShimmer_FOR_SYSTEM_SPINNER: lighten(palette.teal, 0.2), + permission: rgb(palette.tealSoft), + permissionShimmer: lighten(palette.tealSoft, 0.25), + planMode: rgb(palette.teal), + ide: rgb(palette.tealSoft), + promptBorder: rgb(palette.border), + promptBorderShimmer: rgb(palette.borderGlow), + text: rgb(palette.text), + inverseText: rgb(palette.base), + inactive: rgb(palette.textDim), + subtle: mix(palette.base, palette.orange, 0.08), + suggestion: rgb(palette.orangeSoft), + remember: rgb(palette.amber), + background: rgb(palette.base), + success: rgb(palette.green), + error: rgb(palette.red), + warning: rgb(palette.amber), + warningShimmer: lighten(palette.amber, 0.28), + diffAdded: mix(palette.base, palette.green, 0.15), + diffRemoved: mix(palette.base, palette.red, 0.15), + diffAddedDimmed: mix(palette.base, palette.green, 0.08), + diffRemovedDimmed: mix(palette.base, palette.red, 0.08), + diffAddedWord: mix(palette.base, palette.green, 0.32), + diffRemovedWord: mix(palette.base, palette.red, 0.32), + diffAddedWordDimmed: mix(palette.base, palette.green, 0.18), + diffRemovedWordDimmed: mix(palette.base, palette.red, 0.18), + red_FOR_SUBAGENTS_ONLY: rgb(palette.red), + blue_FOR_SUBAGENTS_ONLY: rgb(palette.teal), + green_FOR_SUBAGENTS_ONLY: rgb(palette.green), + yellow_FOR_SUBAGENTS_ONLY: rgb(palette.amber), + purple_FOR_SUBAGENTS_ONLY: rgb(palette.purple), + orange_FOR_SUBAGENTS_ONLY: rgb(palette.orange), + pink_FOR_SUBAGENTS_ONLY: lighten(palette.purple, 0.32), + cyan_FOR_SUBAGENTS_ONLY: rgb(palette.tealSoft), + professionalBlue: rgb(palette.teal), + rainbow_red: rgb(palette.red), + rainbow_orange: rgb(palette.orange), + rainbow_yellow: rgb(palette.amber), + rainbow_green: rgb(palette.green), + rainbow_blue: rgb(palette.tealSoft), + rainbow_indigo: rgb(palette.tealDeep), + rainbow_violet: rgb(palette.purple), + rainbow_red_shimmer: lighten(palette.red, 0.35), + rainbow_orange_shimmer: lighten(palette.orange, 0.35), + rainbow_yellow_shimmer: lighten(palette.amber, 0.3), + rainbow_green_shimmer: lighten(palette.green, 0.35), + rainbow_blue_shimmer: lighten(palette.tealSoft, 0.35), + rainbow_indigo_shimmer: lighten(palette.tealDeep, 0.35), + rainbow_violet_shimmer: lighten(palette.purple, 0.35), + clawd_body: rgb(palette.orange), + clawd_background: rgb(palette.base), + userMessageBackground: rgb(palette.panel), + bashMessageBackgroundColor: rgb(palette.surface), + memoryBackgroundColor: mix(palette.panel, palette.teal, 0.08), + rate_limit_fill: rgb(palette.orange), + rate_limit_empty: rgb(palette.borderStrong), + }, +}; + +export const buildBedrockTweakccConfig = (): TweakccConfig => ({ + ccVersion: '', + ccInstallationPath: null, + lastModified: new Date().toISOString(), + changesApplied: false, + hidePiebaldAnnouncement: true, + settings: { + themes: [theme, ...DEFAULT_THEMES], + thinkingVerbs: { + format: '{}... ', + verbs: [ + 'Invoking', + 'Provisioning', + 'Streaming', + 'Scaling', + 'Routing', + 'Deploying', + 'Orchestrating', + 'Processing', + 'Resolving', + 'Synthesizing', + 'Optimizing', + 'Calibrating', + 'Inferencing', + 'Computing', + ], + }, + thinkingStyle: { + updateInterval: 100, + phases: ['▸', '▹', '▸', '▹'], + reverseMirror: false, + }, + userMessageDisplay: { + format: formatUserMessage(getUserLabel()), + styling: ['bold'], + foregroundColor: rgb(palette.text), + backgroundColor: rgb(palette.panel), + borderStyle: 'topBottomBold', + borderColor: rgb(palette.orange), + paddingX: 1, + paddingY: 0, + fitBoxToContent: true, + }, + inputBox: { + removeBorder: true, + }, + misc: { + showTweakccVersion: false, + showPatchesApplied: false, + expandThinkingBlocks: true, + enableConversationTitle: true, + hideStartupBanner: true, + hideCtrlGToEditPrompt: true, + hideStartupClawd: true, + increaseFileReadLimit: true, + }, + toolsets: [ + { + name: 'bedrock', + allowedTools: '*', + }, + ], + defaultToolset: 'bedrock', + planModeToolset: 'bedrock', + }, +}); diff --git a/src/brands/index.ts b/src/brands/index.ts index 0c1db3d..c8cf095 100644 --- a/src/brands/index.ts +++ b/src/brands/index.ts @@ -4,6 +4,7 @@ import { buildMinimaxTweakccConfig } from './minimax.js'; import { buildOpenRouterTweakccConfig } from './openrouter.js'; import { buildCCRouterTweakccConfig } from './ccrouter.js'; import { buildMirrorTweakccConfig } from './mirror.js'; +import { buildBedrockTweakccConfig } from './bedrock.js'; export interface BrandPreset { key: string; @@ -43,6 +44,12 @@ const BRAND_PRESETS: Record = { description: 'Reflective silver/chrome theme for pure Claude Code experience.', buildTweakccConfig: buildMirrorTweakccConfig, }, + bedrock: { + key: 'bedrock', + label: 'Bedrock AWS', + description: 'AWS-themed with orange/amber accents for Amazon Bedrock.', + buildTweakccConfig: buildBedrockTweakccConfig, + }, }; export const listBrandPresets = (): BrandPreset[] => Object.values(BRAND_PRESETS); diff --git a/src/cli/help.ts b/src/cli/help.ts index a9ebfa8..dcb6f87 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -34,9 +34,9 @@ COMMANDS OPTIONS (create/quick) --name Variant name (becomes CLI command) - --provider Provider: mirror | zai | minimax | openrouter | ccrouter + --provider Provider: mirror | zai | minimax | openrouter | ccrouter | bedrock --api-key Provider API key - --brand Theme: auto | none | mirror | zai | minimax + --brand Theme: auto | none | mirror | zai | minimax | bedrock --tui / --no-tui Force TUI on/off OPTIONS (advanced) @@ -57,6 +57,7 @@ PROVIDERS minimax MiniMax-M2.1 via MiniMax Cloud openrouter 100+ models via OpenRouter ccrouter Local LLMs via Claude Code Router + bedrock Claude via AWS Bedrock (requires model flags) EXAMPLES npx cc-mirror quick --provider mirror --name mclaude diff --git a/src/core/wrapper.ts b/src/core/wrapper.ts index 734cb5c..57a5638 100644 --- a/src/core/wrapper.ts +++ b/src/core/wrapper.ts @@ -34,6 +34,11 @@ const C = { mirSecondary: '\x1b[38;5;250m', // Platinum mirAccent: '\x1b[38;5;45m', // Electric cyan mirDim: '\x1b[38;5;243m', // Muted silver + // Bedrock: AWS Orange gradient + brPrimary: '\x1b[38;5;208m', // AWS Orange + brSecondary: '\x1b[38;5;214m', // Light orange + brAccent: '\x1b[38;5;172m', // Dark amber + brDim: '\x1b[38;5;130m', // Muted brown-orange // Default: White/Gray defPrimary: '\x1b[38;5;255m', // White defDim: '\x1b[38;5;245m', // Gray @@ -111,6 +116,19 @@ const SPLASH_ART: SplashArt = { `${C.mirSecondary} Claude ${C.mirDim}━${C.mirSecondary} Pure Reflection${C.reset}`, '', ], + bedrock: [ + '', + `${C.brPrimary} ██████╗ ███████╗██████╗ ██████╗ ██████╗ ██████╗██╗ ██╗${C.reset}`, + `${C.brPrimary} ██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝${C.reset}`, + `${C.brSecondary} ██████╔╝█████╗ ██║ ██║██████╔╝██║ ██║██║ █████╔╝${C.reset}`, + `${C.brSecondary} ██╔══██╗██╔══╝ ██║ ██║██╔══██╗██║ ██║██║ ██╔═██╗${C.reset}`, + `${C.brAccent} ██████╔╝███████╗██████╔╝██║ ██║╚██████╔╝╚██████╗██║ ██╗${C.reset}`, + `${C.brAccent} ╚═════╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝${C.reset}`, + '', + `${C.brDim} ━━━━━━━━━━━━━━${C.brPrimary}◆${C.brDim}━━━━━━━━━━━━━━${C.reset}`, + `${C.brSecondary} Amazon Bedrock ${C.brDim}━${C.brSecondary} Claude${C.reset}`, + '', + ], default: [ '', `${C.defPrimary} ██████╗ ██████╗ ${C.defDim}━━ M I R R O R${C.reset}`, @@ -123,7 +141,7 @@ const SPLASH_ART: SplashArt = { ], }; -const KNOWN_SPLASH_STYLES = ['zai', 'minimax', 'openrouter', 'ccrouter', 'mirror']; +const KNOWN_SPLASH_STYLES = ['zai', 'minimax', 'openrouter', 'ccrouter', 'mirror', 'bedrock']; const buildWindowsWrapperScript = (opts: { configDir: string; @@ -343,6 +361,12 @@ export const writeWrapper = ( 'CCMMIR', ' __cc_show_label="0"', ' ;;', + ' bedrock)', + " cat <<'CCMBED'", + ...SPLASH_ART.bedrock, + 'CCMBED', + ' __cc_show_label="0"', + ' ;;', ' *)', " cat <<'CCMGEN'", ...SPLASH_ART.default, diff --git a/src/providers/index.ts b/src/providers/index.ts index 32cc7b7..2a442d8 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -116,6 +116,28 @@ const PROVIDERS: Record = { requiresModelMapping: false, // Models configured in ~/.claude-code-router/config.json credentialOptional: true, // No API key needed - CCRouter handles auth }, + bedrock: { + key: 'bedrock', + label: 'Amazon Bedrock', + description: 'Claude models via AWS Bedrock', + baseUrl: '', // Empty - uses Bedrock SDK internally (NOT ANTHROPIC_BASE_URL) + env: { + API_TIMEOUT_MS: DEFAULT_TIMEOUT_MS, + // CRITICAL: This enables Bedrock mode in Claude Code + CLAUDE_CODE_USE_BEDROCK: '1', + // Default region (user can override via AWS_REGION env or extraEnv) + AWS_REGION: 'us-east-1', + // Cosmetic settings + CC_MIRROR_SPLASH: 1, + CC_MIRROR_PROVIDER_LABEL: 'Amazon Bedrock', + CC_MIRROR_SPLASH_STYLE: 'bedrock', + }, + apiKeyLabel: '', // Empty - skip credential prompt (AWS auth handled externally) + authMode: 'none', // AWS credentials via SDK credential chain or AWS_BEARER_TOKEN_BEDROCK + credentialOptional: true, + noPromptPack: true, // Pure Claude experience (no tool routing overrides) + requiresModelMapping: true, // User must provide Bedrock model IDs or inference profile ARNs + }, custom: { key: 'custom', label: 'Custom', @@ -180,8 +202,10 @@ export const buildEnv = ({ providerKey, baseUrl, apiKey, extraEnv, modelOverride const env: ProviderEnv = { ...provider.env }; const authMode = provider.authMode ?? 'apiKey'; - // For 'none' authMode, only apply cosmetic env vars - no auth or base URL + // For 'none' authMode, apply model overrides and extraEnv but no auth or base URL if (authMode === 'none') { + // Apply model overrides (for bedrock, openrouter-style model mapping) + applyModelOverrides(env, modelOverrides); // Still allow extraEnv for user customization if (Array.isArray(extraEnv)) { for (const entry of extraEnv) { diff --git a/test/e2e/bedrock.test.ts b/test/e2e/bedrock.test.ts new file mode 100644 index 0000000..e00fc8d --- /dev/null +++ b/test/e2e/bedrock.test.ts @@ -0,0 +1,491 @@ +/** + * E2E Tests - Amazon Bedrock Provider + * + * Tests Bedrock-specific configuration including CLAUDE_CODE_USE_BEDROCK, + * AWS_REGION, model mapping (like OpenRouter), and AWS credential handling. + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import * as core from '../../src/core/index.js'; +import { getWrapperPath, getWrapperScriptPath } from '../../src/core/paths.js'; +import { makeTempDir, readFile, cleanup } from '../helpers/index.js'; + +const isWindows = process.platform === 'win32'; + +// Example Bedrock model IDs +const BEDROCK_MODELS = { + // Regional (us.) prefix + sonnet: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + opus: 'us.anthropic.claude-opus-4-5-20251101-v1:0', + haiku: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', + // Global inference profile prefix + sonnetGlobal: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0', + opusGlobal: 'global.anthropic.claude-opus-4-5-20251101-v1:0', + haikuGlobal: 'global.anthropic.claude-haiku-4-5-20251001-v1:0', +}; + +test('E2E: Bedrock Provider', async (t) => { + const createdDirs: string[] = []; + + t.after(() => { + for (const dir of createdDirs) { + cleanup(dir); + } + }); + + await t.test('CLAUDE_CODE_USE_BEDROCK is set to 1 in settings.json', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-use', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-use'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal( + config.env.CLAUDE_CODE_USE_BEDROCK, + '1', + 'CLAUDE_CODE_USE_BEDROCK must be set to "1" for Bedrock mode' + ); + }); + + await t.test('default AWS_REGION is us-east-1', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-region', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-region'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal(config.env.AWS_REGION, 'us-east-1', 'default AWS_REGION should be us-east-1'); + }); + + await t.test('no model IDs are hardcoded (requiresModelMapping)', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-no-defaults', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-no-defaults'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + // No hardcoded model IDs - user must provide them + assert.ok(!Object.hasOwn(config.env, 'ANTHROPIC_DEFAULT_SONNET_MODEL'), 'No default sonnet model should be set'); + assert.ok(!Object.hasOwn(config.env, 'ANTHROPIC_DEFAULT_OPUS_MODEL'), 'No default opus model should be set'); + assert.ok(!Object.hasOwn(config.env, 'ANTHROPIC_DEFAULT_HAIKU_MODEL'), 'No default haiku model should be set'); + }); + + await t.test('model overrides work via modelOverrides (regional us. prefix)', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-models-regional', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + modelOverrides: { + sonnet: BEDROCK_MODELS.sonnet, + opus: BEDROCK_MODELS.opus, + haiku: BEDROCK_MODELS.haiku, + smallFast: BEDROCK_MODELS.haiku, + }, + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-models-regional'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal(config.env.ANTHROPIC_DEFAULT_SONNET_MODEL, BEDROCK_MODELS.sonnet); + assert.equal(config.env.ANTHROPIC_DEFAULT_OPUS_MODEL, BEDROCK_MODELS.opus); + assert.equal(config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, BEDROCK_MODELS.haiku); + assert.equal(config.env.ANTHROPIC_SMALL_FAST_MODEL, BEDROCK_MODELS.haiku); + }); + + await t.test('model overrides work with global inference profile prefix', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-models-global', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + modelOverrides: { + sonnet: BEDROCK_MODELS.sonnetGlobal, + opus: BEDROCK_MODELS.opusGlobal, + haiku: BEDROCK_MODELS.haikuGlobal, + smallFast: BEDROCK_MODELS.haikuGlobal, + }, + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-models-global'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + // Global prefix enables cross-region routing + assert.equal(config.env.ANTHROPIC_DEFAULT_SONNET_MODEL, BEDROCK_MODELS.sonnetGlobal); + assert.equal(config.env.ANTHROPIC_DEFAULT_OPUS_MODEL, BEDROCK_MODELS.opusGlobal); + assert.equal(config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, BEDROCK_MODELS.haikuGlobal); + assert.equal(config.env.ANTHROPIC_SMALL_FAST_MODEL, BEDROCK_MODELS.haikuGlobal); + }); + + await t.test('custom inference profile ARNs work via modelOverrides', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + const profileArn = 'arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-custom-profile'; + + core.createVariant({ + name: 'test-bedrock-profile-arn', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + modelOverrides: { + sonnet: profileArn, + opus: profileArn, + haiku: profileArn, + }, + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-profile-arn'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal( + config.env.ANTHROPIC_DEFAULT_SONNET_MODEL, + profileArn, + 'custom inference profile ARN should work as model ID' + ); + }); + + await t.test('no ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN set', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-no-auth', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-no-auth'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.ok( + !Object.hasOwn(config.env, 'ANTHROPIC_API_KEY'), + 'ANTHROPIC_API_KEY should not be set (AWS auth handled externally)' + ); + assert.ok( + !Object.hasOwn(config.env, 'ANTHROPIC_AUTH_TOKEN'), + 'ANTHROPIC_AUTH_TOKEN should not be set (AWS auth handled externally)' + ); + }); + + await t.test('no ANTHROPIC_BASE_URL set', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-no-baseurl', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-no-baseurl'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.ok( + !Object.hasOwn(config.env, 'ANTHROPIC_BASE_URL'), + 'ANTHROPIC_BASE_URL should not be set (Bedrock uses SDK internally)' + ); + }); + + await t.test('CC_MIRROR_UNSET_AUTH_TOKEN is NOT set (authMode is none)', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-unset-token', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-unset-token'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.ok( + !Object.hasOwn(config.env, 'CC_MIRROR_UNSET_AUTH_TOKEN'), + 'CC_MIRROR_UNSET_AUTH_TOKEN should not be set for authMode=none' + ); + }); + + await t.test('wrapper script contains bedrock splash art', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-splash', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const wrapperPath = getWrapperPath(binDir, 'test-bedrock-splash'); + const scriptPath = getWrapperScriptPath(binDir, 'test-bedrock-splash'); + const wrapperContent = readFile(isWindows ? scriptPath : wrapperPath); + + // Verify bedrock case is in wrapper + if (isWindows) { + assert.ok(wrapperContent.includes('"bedrock"'), 'wrapper should include bedrock splash style'); + } else { + assert.ok(wrapperContent.includes('bedrock)'), 'wrapper should have case for bedrock splash style'); + } + + // Verify Bedrock ASCII art is present (check for the tagline) + assert.ok(wrapperContent.includes('Amazon Bedrock'), 'wrapper should contain Amazon Bedrock in splash art'); + }); + + await t.test('AWS_REGION can be overridden via extraEnv', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-region-override', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + extraEnv: ['AWS_REGION=eu-west-1'], + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-region-override'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal(config.env.AWS_REGION, 'eu-west-1', 'AWS_REGION should be overridden via extraEnv'); + }); + + await t.test('AWS_BEARER_TOKEN_BEDROCK can be passed through via extraEnv', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-bearer', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + extraEnv: ['AWS_BEARER_TOKEN_BEDROCK=test-bearer-token'], + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-bearer'); + const configPath = path.join(variantDir, 'config', 'settings.json'); + const config = JSON.parse(readFile(configPath)) as { env: Record }; + + assert.equal( + config.env.AWS_BEARER_TOKEN_BEDROCK, + 'test-bearer-token', + 'AWS_BEARER_TOKEN_BEDROCK should be passed through' + ); + }); + + await t.test('bedrock brand has correct theme ID', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-theme', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-theme'); + const tweakConfigPath = path.join(variantDir, 'tweakcc', 'config.json'); + const tweakConfig = JSON.parse(readFile(tweakConfigPath)) as { + settings?: { themes?: { id?: string }[] }; + }; + + assert.equal( + tweakConfig.settings?.themes?.[0]?.id, + 'bedrock-aws', + 'bedrock brand should have bedrock-aws theme ID' + ); + }); + + await t.test('bedrock toolset has no blocked tools', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-toolset', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantDir = path.join(rootDir, 'test-bedrock-toolset'); + const tweakConfigPath = path.join(variantDir, 'tweakcc', 'config.json'); + const tweakConfig = JSON.parse(readFile(tweakConfigPath)); + const bedrockToolset = tweakConfig.settings?.toolsets?.find((t: { name: string }) => t.name === 'bedrock'); + + assert.ok(bedrockToolset, 'bedrock toolset should exist'); + assert.equal(bedrockToolset.allowedTools, '*', 'bedrock should allow all tools'); + assert.ok( + !bedrockToolset.blockedTools || bedrockToolset.blockedTools.length === 0, + 'bedrock should have no blocked tools' + ); + }); + + await t.test('variant.json has correct provider metadata', () => { + const rootDir = makeTempDir(); + const binDir = makeTempDir(); + createdDirs.push(rootDir, binDir); + + core.createVariant({ + name: 'test-bedrock-meta', + providerKey: 'bedrock', + apiKey: '', + rootDir, + binDir, + brand: 'bedrock', + promptPack: false, + skillInstall: false, + noTweak: true, + tweakccStdio: 'pipe', + }); + + const variantMetaPath = path.join(rootDir, 'test-bedrock-meta', 'variant.json'); + const meta = JSON.parse(readFile(variantMetaPath)) as { + name: string; + provider: string; + }; + + assert.equal(meta.name, 'test-bedrock-meta'); + assert.equal(meta.provider, 'bedrock'); + }); +}); diff --git a/test/e2e/providers.ts b/test/e2e/providers.ts index b4a3c1b..d695c2b 100644 --- a/test/e2e/providers.ts +++ b/test/e2e/providers.ts @@ -35,4 +35,12 @@ export const PROVIDERS = [ expectedSplashStyle: 'ccrouter', colorCode: '\\x1b[38;5;39m', // Sky blue }, + { + key: 'bedrock', + name: 'Amazon Bedrock', + apiKey: '', // No API key (AWS credentials via SDK) + expectedThemeId: 'bedrock-aws', + expectedSplashStyle: 'bedrock', + colorCode: '\\x1b[38;5;208m', // AWS Orange + }, ];