diff --git a/src/common/components/icons/vendors/BedrockIcon.tsx b/src/common/components/icons/vendors/BedrockIcon.tsx
new file mode 100644
index 000000000..a65a42c42
--- /dev/null
+++ b/src/common/components/icons/vendors/BedrockIcon.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react';
+
+import { SvgIcon, SvgIconProps } from '@mui/joy';
+
+export function BedrockIcon(props: SvgIconProps) {
+ return
+ {/* AWS-style smile arrow mark */}
+
+
+
+
+ ;
+}
diff --git a/src/modules/aix/server/api/aix.wiretypes.ts b/src/modules/aix/server/api/aix.wiretypes.ts
index b4e9aaec0..12efa9beb 100644
--- a/src/modules/aix/server/api/aix.wiretypes.ts
+++ b/src/modules/aix/server/api/aix.wiretypes.ts
@@ -4,6 +4,7 @@ import * as z from 'zod/v4';
import type { DMessageToolResponsePart } from '~/common/stores/chat/chat.fragments';
import { anthropicAccessSchema } from '~/modules/llms/server/anthropic/anthropic.access';
+import { bedrockAccessSchema } from '~/modules/llms/server/bedrock/bedrock.access';
import { geminiAccessSchema } from '~/modules/llms/server/gemini/gemini.access';
import { ollamaAccessSchema } from '~/modules/llms/server/ollama/ollama.access';
import { openAIAccessSchema } from '~/modules/llms/server/openai/openai.access';
@@ -415,6 +416,7 @@ export namespace AixWire_API {
export const Access_schema = z.discriminatedUnion('dialect', [
anthropicAccessSchema,
+ bedrockAccessSchema,
geminiAccessSchema,
ollamaAccessSchema,
openAIAccessSchema,
@@ -482,6 +484,9 @@ export namespace AixWire_API {
vndAntWebFetch: z.enum(['auto']).optional(),
vndAntWebSearch: z.enum(['auto']).optional(),
+ // Bedrock
+ vndBedrockInvokeAPI: z.enum(['invoke-anthropic', 'converse']).optional(),
+
// Gemini
vndGeminiAspectRatio: z.enum(['1:1', '2:3', '3:2', '3:4', '4:3', '9:16', '16:9', '21:9']).optional(),
vndGeminiCodeExecution: z.enum(['auto']).optional(),
diff --git a/src/modules/backend/backend.router.ts b/src/modules/backend/backend.router.ts
index e0bb6479d..f1da41a05 100644
--- a/src/modules/backend/backend.router.ts
+++ b/src/modules/backend/backend.router.ts
@@ -51,6 +51,7 @@ export const backendRouter = createTRPCRouter({
hasLlmAlibaba: !!env.ALIBABA_API_KEY || !!env.ALIBABA_API_HOST,
hasLlmAnthropic: !!env.ANTHROPIC_API_KEY,
hasLlmAzureOpenAI: !!env.AZURE_OPENAI_API_KEY && !!env.AZURE_OPENAI_API_ENDPOINT,
+ hasLlmBedrock: !!env.BEDROCK_ACCESS_KEY_ID && !!env.BEDROCK_SECRET_ACCESS_KEY,
hasLlmDeepseek: !!env.DEEPSEEK_API_KEY,
hasLlmGemini: !!env.GEMINI_API_KEY,
hasLlmGroq: !!env.GROQ_API_KEY,
diff --git a/src/modules/backend/store-backend-capabilities.ts b/src/modules/backend/store-backend-capabilities.ts
index 7bbbf77dd..438afee01 100644
--- a/src/modules/backend/store-backend-capabilities.ts
+++ b/src/modules/backend/store-backend-capabilities.ts
@@ -11,6 +11,7 @@ export interface BackendCapabilities {
hasLlmAlibaba: boolean;
hasLlmAnthropic: boolean;
hasLlmAzureOpenAI: boolean;
+ hasLlmBedrock: boolean;
hasLlmDeepseek: boolean;
hasLlmGemini: boolean;
hasLlmGroq: boolean;
@@ -51,6 +52,7 @@ const useBackendCapabilitiesStore = create()(
// initial values
hasLlmAlibaba: false,
hasLlmAnthropic: false,
+ hasLlmBedrock: false,
hasLlmAzureOpenAI: false,
hasLlmDeepseek: false,
hasLlmGemini: false,
diff --git a/src/modules/llms/components/LLMVendorIcon.tsx b/src/modules/llms/components/LLMVendorIcon.tsx
index 67f384386..7db866e9b 100644
--- a/src/modules/llms/components/LLMVendorIcon.tsx
+++ b/src/modules/llms/components/LLMVendorIcon.tsx
@@ -12,6 +12,7 @@ import { PhRobot } from '~/common/components/icons/phosphor/PhRobot';
import { AlibabaCloudIcon } from '~/common/components/icons/vendors/AlibabaCloudIcon';
import { AnthropicIcon } from '~/common/components/icons/vendors/AnthropicIcon';
import { AzureIcon } from '~/common/components/icons/vendors/AzureIcon';
+import { BedrockIcon } from '~/common/components/icons/vendors/BedrockIcon';
import { DeepseekIcon } from '~/common/components/icons/vendors/DeepseekIcon';
import { GeminiIcon } from '~/common/components/icons/vendors/GeminiIcon';
import { GroqIcon } from '~/common/components/icons/vendors/GroqIcon';
@@ -38,6 +39,7 @@ const vendorIcons: Record>
alibaba: AlibabaCloudIcon,
anthropic: AnthropicIcon,
azure: AzureIcon,
+ bedrock: BedrockIcon,
deepseek: DeepseekIcon,
googleai: GeminiIcon,
groq: GroqIcon,
diff --git a/src/modules/llms/components/LLMVendorIconSprite.tsx b/src/modules/llms/components/LLMVendorIconSprite.tsx
index cabd0fce4..838c2ee5a 100644
--- a/src/modules/llms/components/LLMVendorIconSprite.tsx
+++ b/src/modules/llms/components/LLMVendorIconSprite.tsx
@@ -15,6 +15,7 @@ const VI: Record = {
alibaba: 'vi-alibaba',
anthropic: 'vi-anthropic',
azure: 'vi-azure',
+ bedrock: 'vi-bedrock',
deepseek: 'vi-deepseek',
googleai: 'vi-googleai',
groq: 'vi-groq',
@@ -66,6 +67,16 @@ export const VendorIconSpriteMemo = React.memo(function VendorIconSprite() {
+
+
+ {/* AWS-style smile arrow mark */}
+
+
+
+
+
+
+
diff --git a/src/modules/llms/components/LLMVendorSetup.tsx b/src/modules/llms/components/LLMVendorSetup.tsx
index 62b3c03e1..6d87153e1 100644
--- a/src/modules/llms/components/LLMVendorSetup.tsx
+++ b/src/modules/llms/components/LLMVendorSetup.tsx
@@ -9,6 +9,7 @@ import { findModelVendor, ModelVendorId } from '../vendors/vendors.registry';
import { AlibabaServiceSetup } from '../vendors/alibaba/AlibabaServiceSetup';
import { AnthropicServiceSetup } from '../vendors/anthropic/AnthropicServiceSetup';
import { AzureServiceSetup } from '../vendors/azure/AzureServiceSetup';
+import { BedrockServiceSetup } from '../vendors/bedrock/BedrockServiceSetup';
import { DeepseekAIServiceSetup } from '../vendors/deepseek/DeepseekAIServiceSetup';
import { GeminiServiceSetup } from '../vendors/gemini/GeminiServiceSetup';
import { GroqServiceSetup } from '../vendors/groq/GroqServiceSetup';
@@ -35,6 +36,7 @@ const vendorSetupComponents: Record m.id === anthropicId);
+}
+
+function _llmBedrockToAnthropicModelId(bedrockBaseId: string): string | undefined {
+ if (!bedrockBaseId.startsWith('anthropic.')) return undefined;
+ // e.g. anthropic.claude-opus-4-6-v1 -> claude-opus-4-6; anthropic.claude-opus-4-5-20251101-v1:0 -> claude-opus-4-5-20251101
+ return bedrockBaseId.slice('anthropic.'.length).replace(/-v\d+(:\d+)?$/, '');
+}
+
+// Bedrock supports these interfaces (no Anthropic web tools)
+const _BEDROCK_ANT_IF_ALLOWLIST: ReadonlySet = new Set([
+ LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_OAI_Reasoning,
+ LLM_IF_ANT_PromptCaching,
+] as const);
+
+// NOTE: llmVndAntInfSpeed not available on Bedrock, llmVndAntWebFetch/llmVndAntSkills not available
+const _BEDROCK_ANT_PARAM_ALLOWLIST: ReadonlySet = new Set([
+ // supported
+ 'llmVndAnt1MContext',
+ 'llmVndAntEffort',
+ 'llmVndAntThinkingBudget',
+ // Not supported by Bedrock
+ // 'llmVndAntInfSpeed', // Bad Request - speed: Extra inputs are not permitted
+ // 'llmVndAntSkills', // code execution is not supported: https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#platform-availability
+ // 'llmVndAntWebFetch', // Bad Request - tools.0: Input tag 'web_fetch_20250910' found using 'type' does not match any of the expected tags: 'bash_20250124', 'custom', 'text_editor_20250124', 'text_editor_20250429', 'text_editor_20250728', 'web_search_20250305'
+ // 'llmVndAntWebSearch', // Bedrock should support web search, but we get 'Bad Request' if the 'web_search_20250305' tool is added
+] as const satisfies DModelParameterId[]);
+
+/** Strip unsupported interfaces and params from an Anthropic model for Bedrock */
+export function llmBedrockStripAnthropicMDS(model: ModelDescriptionSchema): ModelDescriptionSchema {
+ if (!model.parameterSpecs && !model.interfaces.some(i => !_BEDROCK_ANT_IF_ALLOWLIST.has(i)))
+ return model; // nothing to filter
+ return {
+ ...model,
+ interfaces: model.interfaces.filter(i => _BEDROCK_ANT_IF_ALLOWLIST.has(i)),
+ ...(model.parameterSpecs ? {
+ parameterSpecs: model.parameterSpecs.filter(spec => _BEDROCK_ANT_PARAM_ALLOWLIST.has(spec.paramId)),
+ } : {}),
+ };
+}
+
+
// -- Anthropic-through-OpenRouter Vendor Lookup --
const _ORT_ANT_IF_ALLOWLIST: ReadonlySet = new Set([
diff --git a/src/modules/llms/server/bedrock/bedrock.access.ts b/src/modules/llms/server/bedrock/bedrock.access.ts
new file mode 100644
index 000000000..6a937b3bd
--- /dev/null
+++ b/src/modules/llms/server/bedrock/bedrock.access.ts
@@ -0,0 +1,106 @@
+/**
+ * Isomorphic AWS Bedrock API access - SigV4 signing via aws4fetch.
+ *
+ * This module provides the access schema and signing logic for AWS Bedrock API calls.
+ * It supports both the `bedrock` control plane (model listing) and `bedrock-runtime`
+ * data plane (model invocation).
+ *
+ * Authentication uses explicit AWS credentials only (no credential chain) for
+ * Edge Runtime compatibility. aws4fetch auto-detects service and region from URLs.
+ */
+
+import * as z from 'zod/v4';
+import { TRPCError } from '@trpc/server';
+
+import { AwsClient } from 'aws4fetch';
+
+import { env } from '~/server/env.server';
+
+
+// --- Schema ---
+
+export type BedrockAccessSchema = z.infer;
+export const bedrockAccessSchema = z.object({
+ dialect: z.literal('bedrock'),
+ bedrockAccessKeyId: z.string().trim(),
+ bedrockSecretAccessKey: z.string().trim(),
+ bedrockSessionToken: z.string().trim().nullable(),
+ bedrockRegion: z.string().trim(),
+ clientSideFetch: z.boolean(),
+});
+
+
+// --- Credential & Region Resolution ---
+
+/**
+ * Resolve all Bedrock access config: credentials (all-or-none) + region.
+ * If the user provides an access key, all credentials come from user; otherwise all from env.
+ */
+export function bedrockServerConfig(access: BedrockAccessSchema) {
+ const userProvided = !!access.bedrockAccessKeyId;
+ const accessKeyId = userProvided ? access.bedrockAccessKeyId : (env.BEDROCK_ACCESS_KEY_ID || '');
+ const secretAccessKey = userProvided ? access.bedrockSecretAccessKey : (env.BEDROCK_SECRET_ACCESS_KEY || '');
+ const sessionToken = (userProvided ? access.bedrockSessionToken : env.BEDROCK_SESSION_TOKEN) || undefined;
+ const region = (userProvided ? access.bedrockRegion : env.BEDROCK_REGION) || 'us-east-1';
+
+ if (!accessKeyId || !secretAccessKey)
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'Missing AWS credentials. Add your Access Key ID and Secret Access Key on the UI (Models Setup) or server side (your deployment).',
+ });
+
+ return { accessKeyId, secretAccessKey, sessionToken, region };
+}
+
+
+// --- URLs ---
+
+export function bedrockURLRuntime(region: string, modelId: string, streaming: boolean): string {
+ const action = streaming ? 'invoke-with-response-stream' : 'invoke';
+ return `https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/${action}`;
+}
+
+export function bedrockURLControlPlane(region: string, path: string): string {
+ return `https://bedrock.${region}.amazonaws.com${path}`;
+}
+
+
+// --- Bedrock Access (async SigV4) ---
+
+/**
+ * Signs a request for AWS Bedrock using SigV4 via aws4fetch.
+ * Returns { headers, url } like anthropicAccess/geminiAccess, but async due to SigV4 signing.
+ */
+export async function bedrockAccessAsync(
+ access: BedrockAccessSchema,
+ method: 'GET' | 'POST',
+ url: string,
+ body?: object,
+): Promise<{ headers: HeadersInit; url: string }> {
+
+ const { accessKeyId, secretAccessKey, sessionToken, region } = bedrockServerConfig(access);
+
+ const awsClient = new AwsClient({
+ service: 'bedrock', // Bedrock uses 'bedrock' as the SigV4 service name for both control plane and runtime
+ accessKeyId,
+ secretAccessKey,
+ sessionToken,
+ region,
+ });
+
+ // sign the request - uses SubtleCrypto
+ const signedRequest = await awsClient.sign(url, {
+ method,
+ headers: {
+ 'Accept': 'application/json',
+ ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}),
+ },
+ ...(body ? { body: JSON.stringify(body) } : {}),
+ } satisfies RequestInit);
+
+ // extract headers from the signed Request
+ const headers: Record = {};
+ signedRequest.headers.forEach((value, key) => headers[key] = value);
+
+ return { headers, url };
+}
diff --git a/src/modules/llms/server/bedrock/bedrock.models.ts b/src/modules/llms/server/bedrock/bedrock.models.ts
new file mode 100644
index 000000000..2f09c7e4d
--- /dev/null
+++ b/src/modules/llms/server/bedrock/bedrock.models.ts
@@ -0,0 +1,261 @@
+/**
+ * Bedrock model definitions and ID mapping.
+ *
+ * Bedrock model IDs differ from Anthropic direct API IDs:
+ * - Anthropic direct: 'claude-opus-4-6'
+ * - Bedrock foundation: 'anthropic.claude-opus-4-6-v1'
+ * - Bedrock inference profile: 'us.anthropic.claude-opus-4-6-v1' / 'global.anthropic.claude-opus-4-6-v1'
+ *
+ * For non-Anthropic models (via Converse API), models are identified by their
+ * Bedrock model IDs directly.
+ */
+
+import * as z from 'zod/v4';
+
+import type { ModelDescriptionSchema } from '../llm.server.types';
+
+import { anthropicInjectVariants, llmBedrockFindAnthropicModel, llmBedrockStripAnthropicMDS } from '../anthropic/anthropic.models';
+import { LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
+
+
+// --- Bedrock API Wire Types ---
+
+export namespace BedrockWire_API_Models_List {
+
+ // ListFoundationModels response
+
+ const _FoundationModel_schema = z.object({
+ modelId: z.string(),
+ modelName: z.string(),
+ providerName: z.enum(['Amazon', 'Anthropic', 'Cohere', 'DeepSeek', 'Google', 'Luma AI', 'Meta', 'MiniMax', 'Mistral AI', 'Moonshot AI', 'NVIDIA', 'OpenAI', 'Qwen', 'Stability AI', 'Z.AI']).or(z.string()),
+ inputModalities: z.array(z.enum(['TEXT', 'IMAGE', 'EMBEDDING', 'VIDEO', 'SPEECH']).or(z.string())),
+ outputModalities: z.array(z.enum(['TEXT', 'IMAGE', 'EMBEDDING', 'VIDEO', 'SPEECH']).or(z.string())),
+ responseStreamingSupported: z.boolean().nullable().optional(),
+ inferenceTypesSupported: z.array(z.enum(['ON_DEMAND', 'INFERENCE_PROFILE', 'PROVISIONED']).or(z.string())).optional(),
+ modelLifecycle: z.object({
+ status: z.enum(['ACTIVE', 'LEGACY']).or(z.string()),
+ }).optional(),
+ // Converse API metadata (present on newer models, null on legacy)
+ converse: z.object({
+ maxTokensMaximum: z.number().nullable().optional(),
+ reasoningSupported: z.object({
+ embedded: z.boolean(),
+ }).nullable().optional(),
+ systemRoleSupported: z.boolean().optional(),
+ userImageTypesSupported: z.array(z.enum(['png', 'jpeg', 'gif', 'webp']).or(z.string())).optional(),
+ }).nullable().optional(),
+ });
+
+ export const FoundationModelsResponse_schema = z.object({
+ modelSummaries: z.array(_FoundationModel_schema),
+ });
+
+ // ListInferenceProfiles response
+
+ const _InferenceProfile_schema = z.object({
+ inferenceProfileId: z.string(),
+ inferenceProfileName: z.string(),
+ description: z.string().optional(),
+ type: z.enum(['SYSTEM_DEFINED', 'APPLICATION']).or(z.string()),
+ status: z.enum(['ACTIVE', 'LEGACY']).or(z.string()).optional(),
+ models: z.array(z.object({
+ modelArn: z.string().optional(),
+ })).optional(),
+ });
+
+ export const InferenceProfilesResponse_schema = z.object({
+ inferenceProfileSummaries: z.array(_InferenceProfile_schema),
+ nextToken: z.string().optional().nullable(),
+ });
+
+}
+
+
+// --- Model ID Helpers ---
+
+const _REGION_PREFIX_RE = /^(us|eu|global|jp|apac)\./;
+
+/** Strip region prefix (us., global., etc.) from a Bedrock model ID, returning a bedrockBaseId */
+function _stripRegionPrefix(bedrockModelId: string): string {
+ return bedrockModelId.replace(_REGION_PREFIX_RE, '');
+}
+
+/** Extract region prefix (us, global, etc.) or undefined if none */
+function _extractRegionPrefix(bedrockModelId: string): string | undefined {
+ return _REGION_PREFIX_RE.exec(bedrockModelId)?.[1];
+}
+
+/** Check if a Bedrock model ID is an Anthropic model */
+function _seemsAnthropicBedrockModel(bedrockModelId: string): boolean {
+ return _stripRegionPrefix(bedrockModelId).startsWith('anthropic.');
+}
+
+
+// --- Model Description Building ---
+
+/**
+ * Convert Bedrock model listings into ModelDescriptionSchema[].
+ * Enriches known Anthropic models with hardcoded definitions.
+ * Non-Anthropic models get basic descriptions with Converse API marker.
+ */
+export function bedrockModelsToDescriptions(
+ foundationModels: z.infer,
+ inferenceProfiles: z.infer,
+): ModelDescriptionSchema[] {
+
+ // Collect unique model IDs from both sources
+ const modelMap = new Map();
+
+ // Foundation Models
+ for (const fm of foundationModels.modelSummaries) {
+ // exclude legacy models
+ if (fm.modelLifecycle?.status === 'LEGACY') continue;
+
+ // excludes embedding, image gen, video gen, speech-only
+ if (!fm.inputModalities?.includes('TEXT') || !fm.outputModalities?.includes('TEXT')) continue;
+
+ // denylist '..match..'
+ if (['rerank'].some(match => fm.modelId.includes(match))) continue;
+
+ modelMap.set(fm.modelId, {
+ id: fm.modelId,
+ label: fm.modelName,
+ provider: fm.providerName,
+ isInferenceProfile: false,
+ streaming: fm.responseStreamingSupported ?? true,
+ converseMaxTokens: fm.converse?.maxTokensMaximum ?? null,
+ converseImageTypes: fm.converse?.userImageTypesSupported ?? [],
+ });
+ }
+
+ // Inference Profiles
+ for (const ip of inferenceProfiles.inferenceProfileSummaries) {
+ // exclude legacy models
+ if (ip.status && ip.status !== 'ACTIVE') continue;
+
+ // denylist 'start..'
+ const baseId = _stripRegionPrefix(ip.inferenceProfileId);
+ if (['stability.'].some(start => baseId.startsWith(start))) continue;
+
+ // check if there's a matching foundation model (not anthropic, we map them differently)
+ const foundationMeta = modelMap.get(baseId);
+ // if (!_seemsAnthropicBedrockModel(ip.inferenceProfileId) && !foundationMeta)
+ // console.log('[Bedrock] No matching foundation model for inference profile', ip.inferenceProfileId);
+
+ modelMap.set(ip.inferenceProfileId, {
+ id: ip.inferenceProfileId,
+ label: ip.inferenceProfileName,
+ provider: _extractProvider(ip.inferenceProfileId),
+ isInferenceProfile: true,
+ streaming: foundationMeta?.streaming ?? true,
+ converseMaxTokens: foundationMeta?.converseMaxTokens ?? null,
+ converseImageTypes: foundationMeta?.converseImageTypes ?? [],
+ });
+ }
+
+ // -> ModelDescriptionSchema[], with Anthropic thinking variants injected inline
+ const descriptions: ModelDescriptionSchema[] = [];
+ for (const [modelId, meta] of modelMap) {
+
+ // Known Anthropic models: enrich with hardcoded definitions + inject thinking variants
+ const antModel = llmBedrockFindAnthropicModel(_stripRegionPrefix(modelId));
+ if (antModel) {
+ const isProfile = !!_extractRegionPrefix(modelId);
+ // Inject variants (returns [variant, base] or [base] if no variant)
+ const withVariants = anthropicInjectVariants([], antModel);
+ for (const variant of withVariants) {
+ const label = isProfile ? _profileLabel(variant.label, modelId) : variant.label;
+ descriptions.push({ ...variant, id: modelId, label });
+ }
+ continue;
+ }
+
+ // Unknown models - these will NOT be accessible, hence the '🚧'. We show them just in case, but maybe we shall not
+ const isAnthropic = _seemsAnthropicBedrockModel(modelId);
+ const hasVision = meta.converseImageTypes.length > 0;
+ descriptions.push({
+ id: modelId,
+ label: '🚧 ' + (meta.isInferenceProfile ? _profileLabel(meta.label, modelId) : meta.label),
+ description: `${meta.provider} model on AWS Bedrock${isAnthropic ? '' : ' (Converse API)'}`,
+ contextWindow: isAnthropic ? 200000 : (meta.converseMaxTokens ? meta.converseMaxTokens * 2 : 32768),
+ maxCompletionTokens: isAnthropic ? 64000 : (meta.converseMaxTokens ?? 4096),
+ interfaces:
+ isAnthropic ? [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching]
+ : hasVision ? [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision]
+ : [LLM_IF_OAI_Chat],
+ hidden: true, // not in our known models DB — hide until verified usable
+ });
+ }
+
+ // Filter interfaces and params to Bedrock-supported subset, then sort
+ const filtered = descriptions.map(llmBedrockStripAnthropicMDS);
+ filtered.sort(_bedrockModelSort);
+ return filtered;
+}
+
+
+// --- Helpers ---
+
+/** Build a profile label: strip redundant region prefix from name, append `· Region` suffix (omit for global) */
+function _profileLabel(name: string, modelId: string): string {
+ const prefix = _extractRegionPrefix(modelId) ?? 'regional';
+ // Strip leading "US ", "GLOBAL ", etc. from the AWS-provided name
+ const cleanName = name.replace(/^(US|EU|GLOBAL|JP|APAC)\s+/i, '');
+ // if (prefix === 'global') return cleanName;
+ // Display-friendly casing: US, EU, Global, etc.
+ const displayPrefix = prefix === 'global' ? 'Global' : prefix.toUpperCase();
+ // return `${cleanName} [${displayPrefix}]`;
+ return `${cleanName} · ${displayPrefix}`;
+}
+
+/** Extract provider name from inference profile ID */
+function _extractProvider(profileId: string): string {
+ // Format: region.provider.model-name -> Provider
+ const parts = _stripRegionPrefix(profileId).split('.');
+ return parts[0] ? parts[0].charAt(0).toUpperCase() + parts[0].slice(1) : 'Unknown';
+}
+
+/** Sort: Anthropic first, then family > class > variant (thinking before plain) > region */
+function _bedrockModelSort(a: ModelDescriptionSchema, b: ModelDescriptionSchema): number {
+ const aIsAnt = _seemsAnthropicBedrockModel(a.id);
+ const bIsAnt = _seemsAnthropicBedrockModel(b.id);
+ if (aIsAnt && !bIsAnt) return -1;
+ if (!aIsAnt && bIsAnt) return 1;
+
+ // Within Anthropic: sort by family precedence
+ const familyPrecedence = ['-4-7-', '-4-6', '-4-5-', '-4-1-', '-4-', '-3-7-', '-3-5-', '-3-'];
+ const classPrecedence = ['-opus-', '-sonnet-', '-haiku-'];
+
+ const getFamilyIdx = (id: string) => familyPrecedence.findIndex(f => id.includes(f));
+ const getClassIdx = (id: string) => classPrecedence.findIndex(c => id.includes(c));
+
+ const familyA = getFamilyIdx(a.id);
+ const familyB = getFamilyIdx(b.id);
+ if (familyA !== familyB) return (familyA === -1 ? 999 : familyA) - (familyB === -1 ? 999 : familyB);
+
+ const classA = getClassIdx(a.id);
+ const classB = getClassIdx(b.id);
+ if (classA !== classB) return (classA === -1 ? 999 : classA) - (classB === -1 ? 999 : classB);
+
+ // Thinking/adaptive variants before plain (idVariant present = variant)
+ const aIsVariant = !!a.idVariant;
+ const bIsVariant = !!b.idVariant;
+ if (aIsVariant && !bIsVariant) return -1;
+ if (!aIsVariant && bIsVariant) return 1;
+
+ // Prefer global > us > eu > regional
+ const prefixOrder = ['global', 'us', 'eu', 'jp', 'apac'];
+ const getPrefixIdx = (id: string) => {
+ const prefix = _extractRegionPrefix(id);
+ return prefix ? prefixOrder.indexOf(prefix) : 999;
+ };
+ return getPrefixIdx(a.id) - getPrefixIdx(b.id);
+}
diff --git a/src/modules/llms/server/bedrock/bedrock.router.ts b/src/modules/llms/server/bedrock/bedrock.router.ts
new file mode 100644
index 000000000..637f59ca5
--- /dev/null
+++ b/src/modules/llms/server/bedrock/bedrock.router.ts
@@ -0,0 +1,33 @@
+import * as z from 'zod/v4';
+
+import { createTRPCRouter, edgeProcedure } from '~/server/trpc/trpc.server';
+
+import { ListModelsResponse_schema } from '../llm.server.types';
+import { listModelsRunDispatch } from '../listModels.dispatch';
+
+import { bedrockAccessSchema } from './bedrock.access';
+
+
+// Input Schemas
+
+const _listModelsInputSchema = z.object({
+ access: bedrockAccessSchema,
+});
+
+
+// Router
+
+export const llmBedrockRouter = createTRPCRouter({
+
+ /* [Bedrock] list models - fetches from ListFoundationModels + ListInferenceProfiles */
+ listModels: edgeProcedure
+ .input(_listModelsInputSchema)
+ .output(ListModelsResponse_schema)
+ .query(async ({ input: { access }, signal }) => {
+
+ const models = await listModelsRunDispatch(access, signal);
+
+ return { models };
+ }),
+
+});
diff --git a/src/modules/llms/server/listModels.dispatch.ts b/src/modules/llms/server/listModels.dispatch.ts
index 5671ecbe4..d1ffbef8c 100644
--- a/src/modules/llms/server/listModels.dispatch.ts
+++ b/src/modules/llms/server/listModels.dispatch.ts
@@ -15,6 +15,10 @@ import { llmDevValidateParameterSpecs_DEV, llmsAutoImplyInterfaces } from './mod
import { anthropicInjectVariants, anthropicValidateModelDefs_DEV, AnthropicWire_API_Models_List, hardcodedAnthropicModels, llmsAntCreatePlaceholderModel } from './anthropic/anthropic.models';
import { ANTHROPIC_API_PATHS, anthropicAccess } from './anthropic/anthropic.access';
+// protocol: Bedrock
+import { bedrockAccessAsync, bedrockURLControlPlane, bedrockServerConfig } from './bedrock/bedrock.access';
+import { bedrockModelsToDescriptions, BedrockWire_API_Models_List } from './bedrock/bedrock.models';
+
// protocol: Gemini
import { GeminiWire_API_Models_List } from '~/modules/aix/server/dispatch/wiretypes/gemini.wiretypes';
import { geminiAccess } from './gemini/gemini.access';
@@ -160,6 +164,46 @@ function _listModelsCreateDispatch(access: AixAPI_Access, signal?: AbortSignal):
});
}
+ case 'bedrock': {
+ return createListModelsDispatch({
+ fetchModels: async () => {
+
+ // construct URLs by region
+ const { region } = bedrockServerConfig(access);
+ const fmUrl = bedrockURLControlPlane(region, '/foundation-models?byInferenceType=ON_DEMAND');
+ const ipUrl = bedrockURLControlPlane(region, '/inference-profiles?typeEquals=SYSTEM_DEFINED&maxResults=1000');
+
+ // sign and fetch both lists in parallel - and unbreak on failure
+ const [fmResponse, ipResponse] = await Promise.all([
+ // Foundation Models
+ bedrockAccessAsync(access, 'GET', fmUrl, undefined)
+ .then(fmAccess => fetchJsonOrTRPCThrow({ ...fmAccess, signal, name: 'Bedrock/FM' }))
+ .catch(error => {
+ console.warn('[Bedrock] Foundation Models list failed:', (error as Error)?.message || error);
+ return { modelSummaries: [] };
+ }),
+ // Inference Profiles
+ bedrockAccessAsync(access, 'GET', ipUrl, undefined)
+ .then(ipAccess => fetchJsonOrTRPCThrow({ ...ipAccess, signal, name: 'Bedrock/IP' }))
+ .catch(error => {
+ console.warn('[Bedrock] Inference Profiles list failed:', (error as Error)?.message || error);
+ return { inferenceProfileSummaries: [] };
+ }),
+ ]);
+
+ _wire?.logResponse(fmResponse);
+ _wire?.logResponse(ipResponse);
+
+ return {
+ foundationModels: BedrockWire_API_Models_List.FoundationModelsResponse_schema.parse(fmResponse),
+ inferenceProfiles: BedrockWire_API_Models_List.InferenceProfilesResponse_schema.parse(ipResponse),
+ };
+ },
+ convertToDescriptions: ({ foundationModels, inferenceProfiles }) =>
+ bedrockModelsToDescriptions(foundationModels, inferenceProfiles),
+ });
+ }
+
case 'gemini': {
return createListModelsDispatch({
fetchModels: async () => {
diff --git a/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx b/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx
new file mode 100644
index 000000000..fb197db84
--- /dev/null
+++ b/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx
@@ -0,0 +1,113 @@
+import * as React from 'react';
+
+import { Autocomplete, AutocompleteOption, Box, FormControl, FormHelperText, FormLabel, Typography } from '@mui/joy';
+
+import { Link } from '~/common/components/Link';
+
+
+interface BedrockRegion {
+ id: string;
+ label: string;
+}
+
+// Bedrock-supported regions enabled by default (no opt-in required).
+// Users can type any region (freeSolo) for opt-in regions.
+// Sources: https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html
+// https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html#regions-opt-in-status
+const BEDROCK_REGIONS: BedrockRegion[] = [
+ // US
+ { id: 'us-east-1', label: 'N. Virginia' },
+ { id: 'us-east-2', label: 'Ohio' },
+ { id: 'us-west-1', label: 'N. California' },
+ { id: 'us-west-2', label: 'Oregon' },
+ // Canada
+ { id: 'ca-central-1', label: 'Canada' },
+ // South America
+ { id: 'sa-east-1', label: 'São Paulo' },
+ // Europe
+ { id: 'eu-central-1', label: 'Frankfurt' },
+ { id: 'eu-north-1', label: 'Stockholm' },
+ { id: 'eu-west-1', label: 'Ireland' },
+ { id: 'eu-west-2', label: 'London' },
+ { id: 'eu-west-3', label: 'Paris' },
+ // Asia Pacific
+ { id: 'ap-northeast-1', label: 'Tokyo' },
+ { id: 'ap-northeast-2', label: 'Seoul' },
+ { id: 'ap-northeast-3', label: 'Osaka' },
+ { id: 'ap-south-1', label: 'Mumbai' },
+ { id: 'ap-southeast-1', label: 'Singapore' },
+ { id: 'ap-southeast-2', label: 'Sydney' },
+];
+
+const DEFAULT_REGION = 'us-west-2';
+
+
+export function BedrockRegionAutocomplete(props: {
+ value: string;
+ onChange: (value: string) => void;
+}) {
+
+ // local input state for freeSolo
+ const [inputValue, setInputValue] = React.useState(props.value || DEFAULT_REGION);
+
+ // sync input when value prop changes externally
+ React.useEffect(() => {
+ setInputValue(props.value || DEFAULT_REGION);
+ }, [props.value]);
+
+ // handlers
+ const handleChange = React.useCallback((_event: unknown, newValue: string | BedrockRegion | null) => {
+ if (newValue === null)
+ props.onChange(DEFAULT_REGION);
+ else if (typeof newValue === 'string')
+ props.onChange(newValue);
+ else
+ props.onChange(newValue.id);
+ }, [props]);
+
+ const handleInputChange = React.useCallback((_event: unknown, newInputValue: string, reason: string) => {
+ if (reason !== 'input')
+ return;
+ setInputValue(newInputValue);
+ props.onChange(newInputValue);
+ }, [props]);
+
+ return (
+
+
+
+ AWS Region
+
+
+ see regions
+
+
+
+ freeSolo
+ openOnFocus
+ clearOnEscape
+ placeholder='us-west-2'
+ options={BEDROCK_REGIONS}
+ getOptionKey={(option) => typeof option === 'string' ? option : option.id}
+ getOptionLabel={(option) => typeof option === 'string' ? option : option.id}
+ isOptionEqualToValue={(option, val) => option.id === (typeof val === 'string' ? val : val.id)}
+ value={BEDROCK_REGIONS.find(r => r.id === props.value) ?? (props.value || null)}
+ onChange={handleChange}
+ inputValue={inputValue}
+ onInputChange={handleInputChange}
+ renderOption={(optionProps, option) => {
+ const { key, ...rest } = optionProps as any;
+ return (
+
+ {option.id}
+ {option.label}
+
+ );
+ }}
+ slotProps={{
+ root: { sx: { boxShadow: 'none' } },
+ }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx b/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx
new file mode 100644
index 000000000..c2ed8e74a
--- /dev/null
+++ b/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx
@@ -0,0 +1,108 @@
+import * as React from 'react';
+
+import { Box, IconButton, Typography } from '@mui/joy';
+import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
+import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
+
+import type { DModelsServiceId } from '~/common/stores/llms/llms.service.types';
+import { ClaudeCrabIcon } from '~/common/components/icons/vendors/ClaudeCrabIcon';
+import { FormInputKey } from '~/common/components/forms/FormInputKey';
+import { InlineError } from '~/common/components/InlineError';
+import { Link } from '~/common/components/Link';
+import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
+
+import { ApproximateCosts } from '../ApproximateCosts';
+import { useLlmUpdateModels } from '../../llm.client.hooks';
+import { useServiceSetup } from '../useServiceSetup';
+
+import { BedrockRegionAutocomplete } from './BedrockRegionAutocomplete';
+import { isValidBedrockAccessKeyId, isValidBedrockSecretAccessKey, ModelVendorBedrock } from './bedrock.vendor';
+
+
+export function BedrockServiceSetup(props: { serviceId: DModelsServiceId }) {
+
+ // external state
+ const { service, serviceAccess, serviceHasCloudTenantConfig, serviceHasLLMs, updateSettings } =
+ useServiceSetup(props.serviceId, ModelVendorBedrock);
+
+ // derived state
+ const { bedrockAccessKeyId, bedrockSecretAccessKey, bedrockSessionToken, bedrockRegion } = serviceAccess;
+ const needsUserKey = !serviceHasCloudTenantConfig;
+
+ // advanced mode
+ // const advanced = useToggleableBoolean(!!bedrockSessionToken);
+ const [showSetupInstructions, setShowSetupInstructions] = React.useState(false);
+
+ const accessKeyValid = isValidBedrockAccessKeyId(bedrockAccessKeyId);
+ const secretKeyValid = isValidBedrockSecretAccessKey(bedrockSecretAccessKey);
+ const accessKeyError = (!!bedrockAccessKeyId) && !accessKeyValid;
+ const secretKeyError = (!!bedrockSecretAccessKey) && !secretKeyValid;
+ const shallFetchSucceed = bedrockAccessKeyId ? (accessKeyValid && secretKeyValid) : !needsUserKey;
+
+ // fetch models
+ const { isFetching, refetch, isError, error } =
+ useLlmUpdateModels(!serviceHasLLMs && shallFetchSucceed, service);
+
+ return <>
+
+
+
+
+
+ Access Claude models through your AWS Bedrock account.
+ Requires an IAM Access Key with Bedrock permissions.
+ {showSetupInstructions && (
+
+ Open the AWS IAM Console
+ Go to Users -> Create user (e.g. big-agi-bedrock, no console access)
+ Attach the AmazonBedrockFullAccess policy
+ Open the user -> Security credentials -> Create access key
+ Select "Application running outside AWS" -> Create
+ Copy the Access Key ID and Secret Access Key (shown only once)
+ Enter them below with your AWS Region
+
+ )}
+
+ setShowSetupInstructions(on => !on)}>
+ {showSetupInstructions ? : }
+
+
+
+
+ updateSettings({ bedrockAccessKeyId: value })}
+ required={needsUserKey} isError={accessKeyError}
+ placeholder='AKIA...'
+ />
+
+ updateSettings({ bedrockSecretAccessKey: value })}
+ required={needsUserKey} isError={secretKeyError}
+ placeholder='wJalr...'
+ />
+
+ updateSettings({ bedrockRegion: value })}
+ />
+
+ {/*{advanced.on && updateSettings({ bedrockSessionToken: text })}*/}
+ {/*/>}*/}
+
+
+ {/**/}
+
+ {isError && }
+
+ >;
+}
diff --git a/src/modules/llms/vendors/bedrock/bedrock.vendor.ts b/src/modules/llms/vendors/bedrock/bedrock.vendor.ts
new file mode 100644
index 000000000..74a469dda
--- /dev/null
+++ b/src/modules/llms/vendors/bedrock/bedrock.vendor.ts
@@ -0,0 +1,57 @@
+import { apiAsync } from '~/common/util/trpc.client';
+
+import type { BedrockAccessSchema } from '../../server/bedrock/bedrock.access';
+import type { IModelVendor } from '../IModelVendor';
+
+
+// validation
+export const isValidBedrockAccessKeyId = (key?: string) => !!key && key.length >= 16;
+export const isValidBedrockSecretAccessKey = (key?: string) => !!key && key.length >= 16;
+
+export interface DBedrockServiceSettings {
+ bedrockAccessKeyId: string;
+ bedrockSecretAccessKey: string;
+ bedrockSessionToken: string;
+ bedrockRegion: string;
+ csf?: boolean;
+}
+
+export const ModelVendorBedrock: IModelVendor = {
+ id: 'bedrock',
+ name: 'AWS Bedrock',
+ displayRank: 14,
+ displayGroup: 'cloud',
+ location: 'cloud',
+ brandColor: '#FF9900', // AWS orange
+ instanceLimit: 1,
+ hasServerConfigKey: 'hasLlmBedrock',
+
+ // NOTE: CSF not supported: Bedrock has no CORS headers
+ csfAvailable: _csfBedrockAvailable,
+
+ // functions
+ initializeSetup: () => ({
+ bedrockAccessKeyId: '',
+ bedrockSecretAccessKey: '',
+ bedrockSessionToken: '',
+ bedrockRegion: 'us-west-2',
+ csf: false,
+ }),
+
+ getTransportAccess: (partialSetup): BedrockAccessSchema => ({
+ dialect: 'bedrock',
+ bedrockAccessKeyId: partialSetup?.bedrockAccessKeyId || '',
+ bedrockSecretAccessKey: partialSetup?.bedrockSecretAccessKey || '',
+ bedrockSessionToken: partialSetup?.bedrockSessionToken || null,
+ bedrockRegion: partialSetup?.bedrockRegion || 'us-west-2',
+ clientSideFetch: _csfBedrockAvailable(partialSetup) && !!partialSetup?.csf,
+ }),
+
+ // List Models
+ rpcUpdateModelsOrThrow: async (access) => await apiAsync.llmBedrock.listModels.query({ access }),
+
+};
+
+function _csfBedrockAvailable(s?: Partial) {
+ return !!s?.bedrockAccessKeyId && !!s?.bedrockSecretAccessKey;
+}
diff --git a/src/modules/llms/vendors/vendors.registry.ts b/src/modules/llms/vendors/vendors.registry.ts
index 998fb2565..7d733586a 100644
--- a/src/modules/llms/vendors/vendors.registry.ts
+++ b/src/modules/llms/vendors/vendors.registry.ts
@@ -3,6 +3,7 @@ import type { AixAPI_Access } from '~/modules/aix/server/api/aix.wiretypes';
import { ModelVendorAlibaba } from './alibaba/alibaba.vendor';
import { ModelVendorAnthropic } from './anthropic/anthropic.vendor';
import { ModelVendorAzure } from './azure/azure.vendor';
+import { ModelVendorBedrock } from './bedrock/bedrock.vendor';
import { ModelVendorDeepseek } from './deepseek/deepseekai.vendor';
import { ModelVendorGemini } from './gemini/gemini.vendor';
import { ModelVendorGroq } from './groq/groq.vendor';
@@ -26,6 +27,7 @@ export type ModelVendorId =
| 'alibaba'
| 'anthropic'
| 'azure'
+ | 'bedrock'
| 'deepseek'
| 'googleai'
| 'groq'
@@ -48,6 +50,7 @@ const MODEL_VENDOR_REGISTRY: Record = {
alibaba: ModelVendorAlibaba,
anthropic: ModelVendorAnthropic,
azure: ModelVendorAzure,
+ bedrock: ModelVendorBedrock,
deepseek: ModelVendorDeepseek,
googleai: ModelVendorGemini,
groq: ModelVendorGroq,
diff --git a/src/server/env.server.ts b/src/server/env.server.ts
index fcf3f4d8f..3c66d3e4a 100644
--- a/src/server/env.server.ts
+++ b/src/server/env.server.ts
@@ -53,6 +53,12 @@ export const env = createEnv({
ANTHROPIC_API_KEY: z.string().optional(),
ANTHROPIC_API_HOST: z.url().optional(),
+ // LLM: AWS Bedrock (using 2 or 3 auth values)
+ BEDROCK_ACCESS_KEY_ID: z.string().optional(),
+ BEDROCK_SECRET_ACCESS_KEY: z.string().optional(),
+ BEDROCK_SESSION_TOKEN: z.string().optional(), // required with the other 2 on corporate accounts sometimes
+ BEDROCK_REGION: z.string().optional(),
+
// LLM: Deepseek AI
DEEPSEEK_API_KEY: z.string().optional(),
diff --git a/src/server/trpc/trpc.router-edge.ts b/src/server/trpc/trpc.router-edge.ts
index 4a29d1f5a..fb5182277 100644
--- a/src/server/trpc/trpc.router-edge.ts
+++ b/src/server/trpc/trpc.router-edge.ts
@@ -5,6 +5,7 @@ import { aixRouter } from '~/modules/aix/server/api/aix.router';
import { backendRouter } from '~/modules/backend/backend.router';
import { googleSearchRouter } from '~/modules/google/search.router';
import { llmAnthropicRouter } from '~/modules/llms/server/anthropic/anthropic.router';
+import { llmBedrockRouter } from '~/modules/llms/server/bedrock/bedrock.router';
import { llmGeminiRouter } from '~/modules/llms/server/gemini/gemini.router';
import { llmOllamaRouter } from '~/modules/llms/server/ollama/ollama.router';
import { llmOpenAIRouter } from '~/modules/llms/server/openai/openai.router';
@@ -19,6 +20,7 @@ export const appRouterEdge = createTRPCRouter({
backend: backendRouter,
googleSearch: googleSearchRouter,
llmAnthropic: llmAnthropicRouter,
+ llmBedrock: llmBedrockRouter,
llmGemini: llmGeminiRouter,
llmOllama: llmOllamaRouter,
llmOpenAI: llmOpenAIRouter,