diff --git a/README.md b/README.md
index c2a25fec0..44f4c54d4 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
**Core Capabilities**
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
+- **AWS Bedrock Support**: Built-in AWS Bedrock provider presets with AKSK and API Key authentication, cross-region inference support (global/us/eu/apac), covering Claude Code and OpenCode
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
diff --git a/README_JA.md b/README_JA.md
index 51e53b6dd..2b69c1cef 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
**コア機能**
- **プロバイダ管理**:Claude Code、Codex、Gemini の API 設定をワンクリックで切り替え
+- **AWS Bedrock 対応**:AWS Bedrock プロバイダプリセットを内蔵、AKSK および API Key 認証に対応、クロスリージョン推論(global/us/eu/apac)をサポート、Claude Code と OpenCode に対応
- **速度テスト**:エンドポイント遅延を計測し、品質を可視化
- **インポート/エクスポート**:設定をバックアップ・復元(最新 10 件を自動ローテーション)
- **多言語対応**:UI/エラー/トレイを含む中国語・英語・日本語ローカライズ
diff --git a/README_ZH.md b/README_ZH.md
index f791d8868..f7beadb3e 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -157,6 +157,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
**核心功能**
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
+- **AWS Bedrock 支持**:内置 AWS Bedrock 供应商预设,支持 AKSK 和 API Key 两种认证方式,支持跨区域推理(global/us/eu/apac),覆盖 Claude Code 和 OpenCode
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx
index 9dd750576..f9ac424c3 100644
--- a/src/components/providers/forms/ClaudeFormFields.tsx
+++ b/src/components/providers/forms/ClaudeFormFields.tsx
@@ -198,8 +198,8 @@ export function ClaudeFormFields({
/>
)}
- {/* API 格式选择(仅非官方供应商显示) */}
- {shouldShowModelSelector && (
+ {/* API 格式选择(仅非官方、非云服务商显示) */}
+ {shouldShowModelSelector && category !== "cloud_provider" && (
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx
index 03f4da197..647e02f10 100644
--- a/src/components/providers/forms/ProviderForm.tsx
+++ b/src/components/providers/forms/ProviderForm.tsx
@@ -40,7 +40,10 @@ import {
import { OpenCodeFormFields } from "./OpenCodeFormFields";
import { OpenClawFormFields } from "./OpenClawFormFields";
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
-import { applyTemplateValues } from "@/utils/providerConfigUtils";
+import {
+ applyTemplateValues,
+ hasApiKeyField,
+} from "@/utils/providerConfigUtils";
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
import { getCodexCustomTemplate } from "@/config/codexTemplates";
import CodexConfigEditor from "./CodexConfigEditor";
@@ -594,7 +597,8 @@ export function ProviderForm({
}
// 非官方供应商必填校验:端点和 API Key
- if (category !== "official") {
+ // cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验
+ if (category !== "official" && category !== "cloud_provider") {
if (appId === "claude") {
if (!baseUrl.trim()) {
toast.error(
@@ -843,7 +847,8 @@ export function ProviderForm({
);
}, [groupedPresets]);
- const shouldShowSpeedTest = category !== "official";
+ const shouldShowSpeedTest =
+ category !== "official" && category !== "cloud_provider";
const {
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
@@ -1233,10 +1238,13 @@ export function ProviderForm({
{appId === "claude" && (
{
try {
const config = JSON.parse(jsonString);
+
+ // 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
+ if (
+ typeof config?.apiKey === "string" &&
+ config.apiKey &&
+ !config.apiKey.includes("${")
+ ) {
+ return config.apiKey;
+ }
+
const env = config?.env;
if (!env) return "";
@@ -112,6 +122,12 @@ export const hasApiKeyField = (
): boolean => {
try {
const config = JSON.parse(jsonString);
+
+ // 检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
+ if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
+ return true;
+ }
+
const env = config?.env ?? {};
if (appType === "gemini") {
@@ -144,6 +160,13 @@ export const setApiKeyInConfig = (
const { createIfMissing = false, appType, apiKeyField } = options;
try {
const config = JSON.parse(jsonString);
+
+ // 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
+ if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
+ config.apiKey = apiKey;
+ return JSON.stringify(config, null, 2);
+ }
+
if (!config.env) {
if (!createIfMissing) return jsonString;
config.env = {};
diff --git a/tests/config/claudeProviderPresets.test.ts b/tests/config/claudeProviderPresets.test.ts
new file mode 100644
index 000000000..dc1f4397c
--- /dev/null
+++ b/tests/config/claudeProviderPresets.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it } from "vitest";
+import { providerPresets } from "@/config/claudeProviderPresets";
+
+describe("AWS Bedrock Provider Presets", () => {
+ const bedrockAksk = providerPresets.find(
+ (p) => p.name === "AWS Bedrock (AKSK)",
+ );
+
+ it("should include AWS Bedrock (AKSK) preset", () => {
+ expect(bedrockAksk).toBeDefined();
+ });
+
+ it("AKSK preset should have required AWS env variables", () => {
+ const env = (bedrockAksk!.settingsConfig as any).env;
+ expect(env).toHaveProperty("AWS_ACCESS_KEY_ID");
+ expect(env).toHaveProperty("AWS_SECRET_ACCESS_KEY");
+ expect(env).toHaveProperty("AWS_REGION");
+ expect(env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
+ });
+
+ it("AKSK preset should have template values for AWS credentials", () => {
+ expect(bedrockAksk!.templateValues).toBeDefined();
+ expect(bedrockAksk!.templateValues!.AWS_ACCESS_KEY_ID).toBeDefined();
+ expect(bedrockAksk!.templateValues!.AWS_SECRET_ACCESS_KEY).toBeDefined();
+ expect(bedrockAksk!.templateValues!.AWS_REGION).toBeDefined();
+ expect(bedrockAksk!.templateValues!.AWS_REGION.editorValue).toBe(
+ "us-west-2",
+ );
+ });
+
+ it("AKSK preset should have correct base URL template", () => {
+ const env = (bedrockAksk!.settingsConfig as any).env;
+ expect(env.ANTHROPIC_BASE_URL).toContain("bedrock-runtime");
+ expect(env.ANTHROPIC_BASE_URL).toContain("${AWS_REGION}");
+ });
+
+ it("AKSK preset should have cloud_provider category", () => {
+ expect(bedrockAksk!.category).toBe("cloud_provider");
+ });
+
+ it("AKSK preset should have Bedrock model as default", () => {
+ const env = (bedrockAksk!.settingsConfig as any).env;
+ expect(env.ANTHROPIC_MODEL).toContain("anthropic.claude");
+ });
+
+ const bedrockApiKey = providerPresets.find(
+ (p) => p.name === "AWS Bedrock (API Key)",
+ );
+
+ it("should include AWS Bedrock (API Key) preset", () => {
+ expect(bedrockApiKey).toBeDefined();
+ });
+
+ it("API Key preset should have apiKey field and AWS env variables", () => {
+ const config = bedrockApiKey!.settingsConfig as any;
+ expect(config).toHaveProperty("apiKey", "");
+ expect(config.env).toHaveProperty("AWS_REGION");
+ expect(config.env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
+ });
+
+ it("API Key preset should NOT have AKSK env variables", () => {
+ const env = (bedrockApiKey!.settingsConfig as any).env;
+ expect(env).not.toHaveProperty("AWS_ACCESS_KEY_ID");
+ expect(env).not.toHaveProperty("AWS_SECRET_ACCESS_KEY");
+ });
+
+ it("API Key preset should have template values for region only", () => {
+ expect(bedrockApiKey!.templateValues).toBeDefined();
+ expect(bedrockApiKey!.templateValues!.AWS_REGION).toBeDefined();
+ expect(bedrockApiKey!.templateValues!.AWS_REGION.editorValue).toBe(
+ "us-west-2",
+ );
+ });
+
+ it("API Key preset should have cloud_provider category", () => {
+ expect(bedrockApiKey!.category).toBe("cloud_provider");
+ });
+});
diff --git a/tests/config/opencodeProviderPresets.test.ts b/tests/config/opencodeProviderPresets.test.ts
new file mode 100644
index 000000000..0a57869a0
--- /dev/null
+++ b/tests/config/opencodeProviderPresets.test.ts
@@ -0,0 +1,68 @@
+import { describe, expect, it } from "vitest";
+import {
+ opencodeProviderPresets,
+ opencodeNpmPackages,
+ OPENCODE_PRESET_MODEL_VARIANTS,
+} from "@/config/opencodeProviderPresets";
+
+describe("AWS Bedrock OpenCode Provider Presets", () => {
+ it("should include @ai-sdk/amazon-bedrock in npm packages", () => {
+ const bedrockPkg = opencodeNpmPackages.find(
+ (p) => p.value === "@ai-sdk/amazon-bedrock",
+ );
+ expect(bedrockPkg).toBeDefined();
+ expect(bedrockPkg!.label).toBe("Amazon Bedrock");
+ });
+
+ it("should include Bedrock model variants", () => {
+ const variants = OPENCODE_PRESET_MODEL_VARIANTS["@ai-sdk/amazon-bedrock"];
+ expect(variants).toBeDefined();
+ expect(variants.length).toBeGreaterThan(0);
+
+ const opusModel = variants.find((v) =>
+ v.id.includes("anthropic.claude-opus-4-6"),
+ );
+ expect(opusModel).toBeDefined();
+ });
+
+ const bedrockPreset = opencodeProviderPresets.find(
+ (p) => p.name === "AWS Bedrock",
+ );
+
+ it("should include AWS Bedrock preset", () => {
+ expect(bedrockPreset).toBeDefined();
+ });
+
+ it("Bedrock preset should use @ai-sdk/amazon-bedrock npm package", () => {
+ expect(bedrockPreset!.settingsConfig.npm).toBe(
+ "@ai-sdk/amazon-bedrock",
+ );
+ });
+
+ it("Bedrock preset should have region in options", () => {
+ expect(bedrockPreset!.settingsConfig.options).toHaveProperty("region");
+ });
+
+ it("Bedrock preset should have cloud_provider category", () => {
+ expect(bedrockPreset!.category).toBe("cloud_provider");
+ });
+
+ it("Bedrock preset should have template values for AWS credentials", () => {
+ expect(bedrockPreset!.templateValues).toBeDefined();
+ expect(bedrockPreset!.templateValues!.region).toBeDefined();
+ expect(bedrockPreset!.templateValues!.region.editorValue).toBe(
+ "us-west-2",
+ );
+ expect(bedrockPreset!.templateValues!.accessKeyId).toBeDefined();
+ expect(bedrockPreset!.templateValues!.secretAccessKey).toBeDefined();
+ });
+
+ it("Bedrock preset should include Claude models", () => {
+ const models = bedrockPreset!.settingsConfig.models;
+ expect(models).toBeDefined();
+ const modelIds = Object.keys(models!);
+ expect(
+ modelIds.some((id) => id.includes("anthropic.claude")),
+ ).toBe(true);
+ });
+});