From c36ff1edfacf07cf7c3b42dbc3e9a8727fd1b99f Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 25 Feb 2026 17:03:00 -0800 Subject: [PATCH] AIX/LLMs: Bedrock: support Bedrock Long-term API Keys --- docs/environment-variables.md | 2 + .../chatGenerate/chatGenerate.dispatch.ts | 4 +- src/modules/backend/backend.router.ts | 2 +- .../llms/server/bedrock/bedrock.access.ts | 95 ++++++++++----- .../llms/server/listModels.dispatch.ts | 4 +- .../bedrock/BedrockRegionAutocomplete.tsx | 12 +- .../vendors/bedrock/BedrockServiceSetup.tsx | 115 +++++++++++++----- .../llms/vendors/bedrock/bedrock.vendor.ts | 20 ++- src/server/env.server.ts | 3 +- 9 files changed, 180 insertions(+), 77 deletions(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index fc0424896..352dedd9c 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -29,6 +29,7 @@ AZURE_OPENAI_API_ENDPOINT= AZURE_OPENAI_API_KEY= ANTHROPIC_API_KEY= ANTHROPIC_API_HOST= +BEDROCK_BEARER_TOKEN= BEDROCK_ACCESS_KEY_ID= BEDROCK_SECRET_ACCESS_KEY= BEDROCK_SESSION_TOKEN= @@ -105,6 +106,7 @@ requiring the user to enter an API key | `AZURE_DEPLOYMENTS_API_VERSION` | API version for the deployments listing endpoint | Optional, defaults to '2023-03-15-preview' | | `ANTHROPIC_API_KEY` | The API key for Anthropic | Optional | | `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, for proxies or custom endpoints | Optional | +| `BEDROCK_BEARER_TOKEN` | Bedrock long-term API key (`ABSK...`). Takes priority over IAM credentials. Short-term keys only work for runtime, not model listing | Optional | | `BEDROCK_ACCESS_KEY_ID` | AWS IAM Access Key ID for Bedrock (Claude models via AWS) | Optional, but if set `BEDROCK_SECRET_ACCESS_KEY` must also be set | | `BEDROCK_SECRET_ACCESS_KEY` | AWS IAM Secret Access Key for Bedrock | Optional, but if set `BEDROCK_ACCESS_KEY_ID` must also be set | | `BEDROCK_SESSION_TOKEN` | AWS Session Token for temporary/STS credentials | Optional | diff --git a/src/modules/aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts b/src/modules/aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts index 3a3344619..c2e7c84dc 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts @@ -1,6 +1,6 @@ import { ANTHROPIC_API_PATHS, anthropicAccess, anthropicBetaFeatures, AnthropicHeaderOptions } from '~/modules/llms/server/anthropic/anthropic.access'; import { OPENAI_API_PATHS, openAIAccess } from '~/modules/llms/server/openai/openai.access'; -import { bedrockAccessAsync, bedrockServerConfig, bedrockURLRuntime } from '~/modules/llms/server/bedrock/bedrock.access'; +import { bedrockAccessAsync, bedrockResolveRegion, bedrockURLRuntime } from '~/modules/llms/server/bedrock/bedrock.access'; import { geminiAccess } from '~/modules/llms/server/gemini/gemini.access'; import { ollamaAccess } from '~/modules/llms/server/ollama/ollama.access'; @@ -90,7 +90,7 @@ export async function createChatGenerateDispatch(access: AixAPI_Access, model: A if (invokeAPI === 'converse') throw new Error('[Bedrock] Converse API is not yet implemented. Use Anthropic models with the InvokeModel API (invoke-anthropic).'); - const region = bedrockServerConfig(access).region; + const region = bedrockResolveRegion(access); const url = bedrockURLRuntime(region, model.id, streaming); // body diff --git a/src/modules/backend/backend.router.ts b/src/modules/backend/backend.router.ts index f1da41a05..2ca0a5914 100644 --- a/src/modules/backend/backend.router.ts +++ b/src/modules/backend/backend.router.ts @@ -51,7 +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, + hasLlmBedrock: !!env.BEDROCK_BEARER_TOKEN || (!!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/llms/server/bedrock/bedrock.access.ts b/src/modules/llms/server/bedrock/bedrock.access.ts index 6a937b3bd..b794d59f4 100644 --- a/src/modules/llms/server/bedrock/bedrock.access.ts +++ b/src/modules/llms/server/bedrock/bedrock.access.ts @@ -1,12 +1,18 @@ /** - * Isomorphic AWS Bedrock API access - SigV4 signing via aws4fetch. + * Isomorphic AWS Bedrock API access - SigV4 signing via aws4fetch + Bearer token auth. * * 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. + * Two authentication modes: + * - **Bearer token**: Simple `Authorization: Bearer ` header (AWS Bedrock API keys). + * Long-term keys (`ABSK...`) support both control plane and runtime. + * UNSUPPORTED: Short-term keys (`bedrock-api-key-...`) only support runtime (not model listing). + * - **SigV4**: Traditional IAM credentials signing via aws4fetch + * + * Priority: client bearer > client IAM > server bearer > server IAM. + * SigV4 uses explicit AWS credentials only (no credential chain) for Edge Runtime compatibility. */ import * as z from 'zod/v4'; @@ -17,11 +23,16 @@ import { AwsClient } from 'aws4fetch'; import { env } from '~/server/env.server'; +// configuration +const DEFAULT_BEDROCK_REGION = 'us-east-1'; // default region for Bedrock, used if not provided by client or env + + // --- Schema --- export type BedrockAccessSchema = z.infer; export const bedrockAccessSchema = z.object({ dialect: z.literal('bedrock'), + bedrockBearerToken: z.string().trim(), bedrockAccessKeyId: z.string().trim(), bedrockSecretAccessKey: z.string().trim(), bedrockSessionToken: z.string().trim().nullable(), @@ -30,26 +41,41 @@ export const bedrockAccessSchema = z.object({ }); -// --- Credential & Region Resolution --- +// --- Auth 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'; +type BedrockAuthBearer = { type: 'bearer'; bearerToken: string; region: string }; +type BedrockAuthSigV4 = { type: 'sigv4'; accessKeyId: string; secretAccessKey: string; sessionToken: string | undefined; region: string }; - 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).', - }); +/** Resolve Bedrock authentication. */ +function _bedrockResolveAuth(access: BedrockAccessSchema): BedrockAuthBearer | BedrockAuthSigV4 { - return { accessKeyId, secretAccessKey, sessionToken, region }; + // 1. Client bearer token (highest priority) + let region = access.bedrockRegion || DEFAULT_BEDROCK_REGION; // client-provided region + if (access.bedrockBearerToken) + return { type: 'bearer', bearerToken: access.bedrockBearerToken, region }; + + // 2. Client IAM credentials + if (access.bedrockAccessKeyId && access.bedrockSecretAccessKey) + return { type: 'sigv4', accessKeyId: access.bedrockAccessKeyId, secretAccessKey: access.bedrockSecretAccessKey, sessionToken: access.bedrockSessionToken || undefined, region }; + + // 3. Server bearer token + region = env.BEDROCK_REGION || DEFAULT_BEDROCK_REGION; // server-provided region (ignores client for security reasons) + if (env.BEDROCK_BEARER_TOKEN) + return { type: 'bearer', bearerToken: env.BEDROCK_BEARER_TOKEN, region }; + + // 4. Server IAM credentials + if (env.BEDROCK_ACCESS_KEY_ID && env.BEDROCK_SECRET_ACCESS_KEY) + return { type: 'sigv4', accessKeyId: env.BEDROCK_ACCESS_KEY_ID, secretAccessKey: env.BEDROCK_SECRET_ACCESS_KEY, sessionToken: env.BEDROCK_SESSION_TOKEN || undefined, region }; + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Missing AWS credentials. Add your Bedrock API Key or IAM Access Key on the UI (Models Setup) or server side (your deployment).', + }); +} + +/** Resolve the Bedrock region from access config. */ +export function bedrockResolveRegion(access: BedrockAccessSchema): string { + return _bedrockResolveAuth(access).region; } @@ -65,11 +91,11 @@ export function bedrockURLControlPlane(region: string, path: string): string { } -// --- Bedrock Access (async SigV4) --- +// --- Bedrock Access (Bearer or async SigV4) --- /** - * Signs a request for AWS Bedrock using SigV4 via aws4fetch. - * Returns { headers, url } like anthropicAccess/geminiAccess, but async due to SigV4 signing. + * Prepares a request for AWS Bedrock using either Bearer token or SigV4 signing. + * Returns { headers, url } like anthropicAccess/geminiAccess, but async due to potential SigV4 signing. */ export async function bedrockAccessAsync( access: BedrockAccessSchema, @@ -78,14 +104,27 @@ export async function bedrockAccessAsync( body?: object, ): Promise<{ headers: HeadersInit; url: string }> { - const { accessKeyId, secretAccessKey, sessionToken, region } = bedrockServerConfig(access); + const auth = _bedrockResolveAuth(access); + // -- Bearer token: simple Authorization header -- + if (auth.type === 'bearer') + return { + headers: { + 'Authorization': `Bearer ${auth.bearerToken}`, + 'Accept': 'application/json', + ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}), + }, + url, + }; + + + // -- SigV4: sign with aws4fetch -- const awsClient = new AwsClient({ service: 'bedrock', // Bedrock uses 'bedrock' as the SigV4 service name for both control plane and runtime - accessKeyId, - secretAccessKey, - sessionToken, - region, + accessKeyId: auth.accessKeyId, + secretAccessKey: auth.secretAccessKey, + sessionToken: auth.sessionToken, + region: auth.region, }); // sign the request - uses SubtleCrypto diff --git a/src/modules/llms/server/listModels.dispatch.ts b/src/modules/llms/server/listModels.dispatch.ts index 73772035c..3e3cccdf6 100644 --- a/src/modules/llms/server/listModels.dispatch.ts +++ b/src/modules/llms/server/listModels.dispatch.ts @@ -16,7 +16,7 @@ import { anthropicInjectVariants, anthropicValidateModelDefs_DEV, AnthropicWire_ import { ANTHROPIC_API_PATHS, anthropicAccess } from './anthropic/anthropic.access'; // protocol: Bedrock -import { bedrockAccessAsync, bedrockURLControlPlane, bedrockServerConfig } from './bedrock/bedrock.access'; +import { bedrockAccessAsync, bedrockResolveRegion, bedrockURLControlPlane } from './bedrock/bedrock.access'; import { bedrockModelsToDescriptions, BedrockWire_API_Models_List } from './bedrock/bedrock.models'; // protocol: Gemini @@ -169,7 +169,7 @@ function _listModelsCreateDispatch(access: AixAPI_Access, signal?: AbortSignal): fetchModels: async () => { // construct URLs by region - const { region } = bedrockServerConfig(access); + const region = bedrockResolveRegion(access); const fmUrl = bedrockURLControlPlane(region, '/foundation-models?byInferenceType=ON_DEMAND'); const ipUrl = bedrockURLControlPlane(region, '/inference-profiles?typeEquals=SYSTEM_DEFINED&maxResults=1000'); diff --git a/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx b/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx index fb197db84..3ff28ff97 100644 --- a/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx +++ b/src/modules/llms/vendors/bedrock/BedrockRegionAutocomplete.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; -import { Autocomplete, AutocompleteOption, Box, FormControl, FormHelperText, FormLabel, Typography } from '@mui/joy'; - -import { Link } from '~/common/components/Link'; +import { Autocomplete, AutocompleteOption, Box, FormControl, FormLabel, Typography } from '@mui/joy'; interface BedrockRegion { @@ -76,11 +74,11 @@ export function BedrockRegionAutocomplete(props: { - AWS Region + AWS Models Region - - see regions - + {/**/} + {/* see regions*/} + {/**/} freeSolo diff --git a/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx b/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx index 3306d892d..b347fec26 100644 --- a/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx +++ b/src/modules/llms/vendors/bedrock/BedrockServiceSetup.tsx @@ -1,10 +1,10 @@ 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 { Box, Divider, IconButton, Typography } from '@mui/joy'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import type { DModelsServiceId } from '~/common/stores/llms/llms.service.types'; +import { AlreadySet } from '~/common/components/AlreadySet'; import { ClaudeCrabIcon } from '~/common/components/icons/vendors/ClaudeCrabIcon'; import { FormInputKey } from '~/common/components/forms/FormInputKey'; import { InlineError } from '~/common/components/InlineError'; @@ -16,7 +16,21 @@ import { useLlmUpdateModels } from '../../llm.client.hooks'; import { useServiceSetup } from '../useServiceSetup'; import { BedrockRegionAutocomplete } from './BedrockRegionAutocomplete'; -import { isValidBedrockAccessKeyId, isValidBedrockSecretAccessKey, ModelVendorBedrock } from './bedrock.vendor'; +import { isValidBedrockAccessKeyId, isValidBedrockBearerToken, isValidBedrockSecretAccessKey, ModelVendorBedrock } from './bedrock.vendor'; + + +const _styles = { + iamGrid: { + display: 'grid', + gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, + gap: 'var(--Card-padding, 1rem)', + }, + iamSectionDimmed: { + opacity: 0.5, + pointerEvents: 'none', + transition: 'opacity 0.2s', + }, +} as const; export function BedrockServiceSetup(props: { serviceId: DModelsServiceId }) { @@ -26,18 +40,33 @@ export function BedrockServiceSetup(props: { serviceId: DModelsServiceId }) { useServiceSetup(props.serviceId, ModelVendorBedrock); // derived state - const { bedrockAccessKeyId, bedrockSecretAccessKey, bedrockSessionToken, bedrockRegion } = serviceAccess; + const { bedrockAccessKeyId, bedrockSecretAccessKey, bedrockBearerToken, bedrockRegion } = serviceAccess; const needsUserKey = !serviceHasCloudTenantConfig; // advanced mode // const advanced = useToggleableBoolean(!!bedrockSessionToken); const [showSetupInstructions, setShowSetupInstructions] = React.useState(false); + // Bearer validation + const bearerTokenValid = isValidBedrockBearerToken(bedrockBearerToken); + const bearerTokenError = !!bedrockBearerToken && !bearerTokenValid; + + // IAM validation const accessKeyValid = isValidBedrockAccessKeyId(bedrockAccessKeyId); const secretKeyValid = isValidBedrockSecretAccessKey(bedrockSecretAccessKey); - const accessKeyError = (!!bedrockAccessKeyId) && !accessKeyValid; - const secretKeyError = (!!bedrockSecretAccessKey) && !secretKeyValid; - const shallFetchSucceed = bedrockAccessKeyId ? (accessKeyValid && secretKeyValid) : !needsUserKey; + const accessKeyError = !!bedrockAccessKeyId && !accessKeyValid; + const secretKeyError = !!bedrockSecretAccessKey && !secretKeyValid; + + // bearer token takes implicit priority - dim IAM when bearer is active + const hasBearer = !!bedrockBearerToken; + // const hasIAM = !!bedrockAccessKeyId || !!bedrockSecretAccessKey; + + + // fetch: succeed if valid client credentials exist, or server-configured + const shallFetchSucceed = + bedrockBearerToken ? bearerTokenValid + : bedrockAccessKeyId ? (accessKeyValid && secretKeyValid) + : !needsUserKey; // fetch models const { isFetching, refetch, isError, error } = @@ -49,40 +78,63 @@ export function BedrockServiceSetup(props: { serviceId: DModelsServiceId }) { - Access Claude models through your AWS Bedrock account. - Requires an IAM Access Key with Bedrock permissions - Bedrock does not support search. + Access Claude models through your AWS Bedrock account + using a long-term API Key or IAM credentials. {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
  • +
  • API Key: in the Bedrock Console, create a long-term key (ABSK...). Short-term keys don't support model listing
  • +
  • AWS IAM: in the IAM Console, create a user with the AmazonBedrockFullAccess permission policy. Open the user, go to Security credentials, then Create access key ("Application running outside AWS") and copy both keys
  • +
  • Select your AWS Models Region below
  • )}
    - setShowSetupInstructions(on => !on)}> - {showSetupInstructions ? : } + setShowSetupInstructions(on => !on)}> +
    + + {/* Bearer token (API Key) - highest client-side priority */} updateSettings({ bedrockAccessKeyId: value })} - required={needsUserKey} isError={accessKeyError} - placeholder='AKIA...' + autoCompleteId='bedrock-bearer-token' label='Bedrock long-term API Key' + rightLabel={<>{needsUserKey + ? undefined + : + }} + value={bedrockBearerToken} onChange={value => updateSettings({ bedrockBearerToken: value })} + required={false} // required={needsUserKey && !hasIAM} + isError={bearerTokenError} + placeholder='ABSK...' /> - updateSettings({ bedrockSecretAccessKey: value })} - required={needsUserKey} isError={secretKeyError} - placeholder='wJalr...' - /> + or + + {/* IAM credentials - side by side on md+, stacked on mobile, dimmed when bearer is active */} + + + {needsUserKey + ? undefined + : + }} + value={bedrockAccessKeyId} onChange={value => updateSettings({ bedrockAccessKeyId: value })} + required={false} // required={needsUserKey && !hasBearer} + isError={accessKeyError} + placeholder='AKIA...' + /> + + updateSettings({ bedrockSecretAccessKey: value })} + required={false} // required={needsUserKey && !hasBearer} + isError={secretKeyError} + placeholder='wJalr...' + /> + + + updateSettings({ bedrockSessionToken: text })}*/} {/*/>}*/} - - {/**/} + {isError && } diff --git a/src/modules/llms/vendors/bedrock/bedrock.vendor.ts b/src/modules/llms/vendors/bedrock/bedrock.vendor.ts index 74a469dda..0e5a21f34 100644 --- a/src/modules/llms/vendors/bedrock/bedrock.vendor.ts +++ b/src/modules/llms/vendors/bedrock/bedrock.vendor.ts @@ -5,10 +5,12 @@ import type { IModelVendor } from '../IModelVendor'; // validation +export const isValidBedrockBearerToken = (key?: string) => !!key && key.length >= 20; export const isValidBedrockAccessKeyId = (key?: string) => !!key && key.length >= 16; export const isValidBedrockSecretAccessKey = (key?: string) => !!key && key.length >= 16; export interface DBedrockServiceSettings { + bedrockBearerToken: string; bedrockAccessKeyId: string; bedrockSecretAccessKey: string; bedrockSessionToken: string; @@ -31,6 +33,7 @@ export const ModelVendorBedrock: IModelVendor ({ + bedrockBearerToken: '', bedrockAccessKeyId: '', bedrockSecretAccessKey: '', bedrockSessionToken: '', @@ -40,9 +43,18 @@ export const ModelVendorBedrock: IModelVendor ({ dialect: 'bedrock', - bedrockAccessKeyId: partialSetup?.bedrockAccessKeyId || '', - bedrockSecretAccessKey: partialSetup?.bedrockSecretAccessKey || '', - bedrockSessionToken: partialSetup?.bedrockSessionToken || null, + // bearer token takes priority; don't send IAM credentials alongside it + ...(partialSetup?.bedrockBearerToken ? { + bedrockBearerToken: partialSetup.bedrockBearerToken, + bedrockAccessKeyId: '', + bedrockSecretAccessKey: '', + bedrockSessionToken: null, + } : { + bedrockBearerToken: '', + bedrockAccessKeyId: partialSetup?.bedrockAccessKeyId || '', + bedrockSecretAccessKey: partialSetup?.bedrockSecretAccessKey || '', + bedrockSessionToken: partialSetup?.bedrockSessionToken || null, + }), bedrockRegion: partialSetup?.bedrockRegion || 'us-west-2', clientSideFetch: _csfBedrockAvailable(partialSetup) && !!partialSetup?.csf, }), @@ -53,5 +65,5 @@ export const ModelVendorBedrock: IModelVendor) { - return !!s?.bedrockAccessKeyId && !!s?.bedrockSecretAccessKey; + return !!s?.bedrockBearerToken || (!!s?.bedrockAccessKeyId && !!s?.bedrockSecretAccessKey); } diff --git a/src/server/env.server.ts b/src/server/env.server.ts index 3c66d3e4a..e9002442d 100644 --- a/src/server/env.server.ts +++ b/src/server/env.server.ts @@ -53,7 +53,8 @@ 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) + // LLM: AWS Bedrock (bearer token OR IAM credentials) + BEDROCK_BEARER_TOKEN: z.string().optional(), // Bedrock long-term API key (ABSK...) - takes priority over IAM credentials; short-term keys only work for runtime, not model listing 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