Swap Your LLM Without a Deploy: Dynamic Model Routing on Bedrock with AWS AppConfig A developer built a model router on AWS AppConfig and Amazon Bedrock that lets users swap large language models without redeploying code. The router uses feature flags to map routing keys like 'fast', 'cheap', and 'open' to model IDs, enabling instant rollbacks and config-only deployments. The implementation includes a Lambda behind API Gateway that reads flags at runtime and routes requests to Claude Haiku, Amazon Nova Micro, or Meta Llama. Bedrock model IDs have a short shelf life. New Claude, Nova, and Llama versions land every few months, pricing changes, and yesterday's best pick becomes today's legacy model. If the model ID is hardcoded in your Lambda, every swap is a code change, a review, and a redeploy — and if the new model misbehaves, rolling back is another deploy. In this hands-on, we'll fix that by building a model router where the model choice lives in AWS AppConfig feature flags, not in code. A Lambda behind API Gateway reads the flags at runtime and routes each request to Claude Haiku, Amazon Nova Micro, or Meta Llama. Swapping a model becomes a config deployment: no code change, no redeploy, instant rollback. Prefer video? This entire hands-on is also on YouTube: curl ?model=fast ──▶ API Gateway ──▶ Lambda ──▶ Amazon Bedrock Converse │ ▲ │ which model id? ▼ │ AWS AppConfig ────────┘ feature flags: fast → Claude Haiku cheap → Nova Micro open → Llama The client asks for a routing key fast , cheap , open — never a model ID. What each key means is decided by whoever controls the AppConfig deployment. AWS AppConfig is a managed feature-flag and configuration service. Three properties make it a good fit for LLM routing: us-east-1 jq installedBedrock models are updated frequently — list what's currently available and use the latest versions, not the ones printed in this article. Anthropic Haiku family aws bedrock list-inference-profiles \ --region us-east-1 \ --query 'inferenceProfileSummaries ?contains inferenceProfileId, haiku .inferenceProfileId' \ --output table Amazon Nova Micro aws bedrock list-inference-profiles \ --region us-east-1 \ --query 'inferenceProfileSummaries ?contains inferenceProfileId, nova-micro .inferenceProfileId' \ --output table Meta Llama aws bedrock list-inference-profiles \ --region us-east-1 \ --query 'inferenceProfileSummaries ?contains inferenceProfileId, llama .inferenceProfileId' \ --output table Export the ones you'll route between replace with the versions listed in your account : export CLAUDE MODEL="us.anthropic.claude-haiku-4-5-20251001-v1:0" export NOVA MODEL="us.amazon.nova-micro-v1:0" export LLAMA MODEL="us.meta.llama4-scout-17b-instruct-v1:0" echo "Claude: $CLAUDE MODEL" echo "Nova : $NOVA MODEL" echo "Llama : $LLAMA MODEL" AppConfig has a small hierarchy: an application contains environments dev, prod, … and configuration profiles the config itself . We create one of each, then a feature-flag document with three flags — each carrying a model id attribute. REGION=us-east-1 1. Application APP ID=$ aws appconfig create-application --region $REGION \ --name bedrock-router --query Id --output text 2. Environment ENV ID=$ aws appconfig create-environment --region $REGION \ --application-id $APP ID --name dev --query Id --output text 3. Configuration Profile feature flag type PROFILE ID=$ aws appconfig create-configuration-profile --region $REGION \ --application-id $APP ID --name model-router \ --location-uri hosted --type "AWS.AppConfig.FeatureFlags" \ --query Id --output text 4. Feature flags jq -n \ --arg c "$CLAUDE MODEL" --arg n "$NOVA MODEL" --arg l "$LLAMA MODEL" \ '{ flags: { fast: {name:"fast", attributes:{model id:{constraints:{type:"string"}}}}, cheap: {name:"cheap", attributes:{model id:{constraints:{type:"string"}}}}, open: {name:"open", attributes:{model id:{constraints:{type:"string"}}}} }, values: { fast: {enabled:true, model id:$c}, cheap: {enabled:true, model id:$n}, open: {enabled:true, model id:$l} }, version: "1" }' /tmp/flags.json aws appconfig create-hosted-configuration-version --region $REGION \ --application-id $APP ID --configuration-profile-id $PROFILE ID \ --content-type "application/json" \ --content fileb:///tmp/flags.json \ /dev/null 5. Deploy using the AWS predefined strategy AppConfig.AllAtOnce aws appconfig start-deployment --region $REGION \ --application-id $APP ID --environment-id $ENV ID \ --deployment-strategy-id AppConfig.AllAtOnce \ --configuration-profile-id $PROFILE ID \ --configuration-version 1 echo "APP ID=$APP ID" echo "ENV ID=$ENV ID" echo "PROFILE ID=$PROFILE ID" AppConfig.AllAtOnce is fine for a dev environment. In production you'd pick a gradual strategy linear or canary with a CloudWatch alarm attached, so a bad config rolls back automatically. Verify what's deployed: aws appconfig get-hosted-configuration-version --region $REGION \ --application-id $APP ID --configuration-profile-id $PROFILE ID \ --version-number 1 \ /tmp/flags out.json /dev/null cat /tmp/flags out.json | jq Create a Lambda Python, name it bedrock-router in the console, then add this inline policy to its execution role IAM → the role → Add permissions → Create inline policy → JSON : { "Version": "2012-10-17", "Statement": { "Sid": "BedrockConverse", "Effect": "Allow", "Action": "bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream" , "Resource": " " }, { "Sid": "AppConfigRead", "Effect": "Allow", "Action": "appconfig:StartConfigurationSession", "appconfig:GetLatestConfiguration" , "Resource": " " } } For production, scope Resource down to your specific models and AppConfig ARNs. python import json import os import time import boto3 REGION = os.environ "AWS REGION" APP ID = os.environ "APPCONFIG APP ID" ENV ID = os.environ "APPCONFIG ENV ID" PROFILE ID = os.environ "APPCONFIG PROFILE ID" appconfigdata = boto3.client "appconfigdata", region name=REGION bedrock = boto3.client "bedrock-runtime", region name=REGION Simple cache to reduce AppConfig calls when the container is reused cache = {"config": None, "token": None, "expires at": 0} CACHE TTL SEC = 30 def load config : now = time.time Return the cached config while it is still valid if cache "config" is not None and now < cache "expires at" : return cache "config" Start a session only on the first call if cache "token" is None: session = appconfigdata.start configuration session ApplicationIdentifier=APP ID, EnvironmentIdentifier=ENV ID, ConfigurationProfileIdentifier=PROFILE ID, cache "token" = session "InitialConfigurationToken" resp = appconfigdata.get latest configuration ConfigurationToken= cache "token" cache "token" = resp "NextPollConfigurationToken" content = resp "Configuration" .read if content: Replace only when there is an update. Keep the current cache if the content is empty cache "config" = json.loads content cache "expires at" = now + CACHE TTL SEC return cache "config" def lambda handler event, context : try: flags = load config Feature flag value map: {"fast":{"enabled":true,"model id":"..."}, ...} qs = event.get "queryStringParameters" or {} model key = qs.get "model", "fast" Default is fast prompt = qs.get "prompt", "Hello. Please introduce yourself in one sentence." flag = flags.get model key if not flag or not flag.get "enabled" : return { "statusCode": 400, "headers": {"Content-Type": "application/json; charset=utf-8"}, "body": json.dumps { "error": f"model key not available: {model key}", "available keys": k for k, v in flags.items if v.get "enabled" , }, ensure ascii=False, , } model id = flag "model id" resp = bedrock.converse modelId=model id, messages= {"role": "user", "content": {"text": prompt} } , inferenceConfig={"maxTokens": 300, "temperature": 0.5}, text = resp "output" "message" "content" 0 "text" return { "statusCode": 200, "headers": {"Content-Type": "application/json; charset=utf-8"}, "body": json.dumps { "model key": model key, "model id": model id, "prompt": prompt, "response": text, "usage": resp.get "usage", {} , }, ensure ascii=False, , } except Exception as e: return { "statusCode": 500, "headers": {"Content-Type": "application/json; charset=utf-8"}, "body": json.dumps {"error": type e . name , "message": str e }, ensure ascii=False, , } Three details worth reading twice: appconfigdata works as a polling session: start configuration session once, then get latest configuration with a token that gets replaced on every call. If nothing changed since the last poll, the response body is if content: . cache survives between invocations. You get near-instant responses and at most one AppConfig poll per 30 seconds per container.Set the Lambda's environment variables Configuration tab → Environment variables → Edit with the values printed in Step 2: echo "APPCONFIG APP ID = $APP ID" echo "APPCONFIG ENV ID = $ENV ID" echo "APPCONFIG PROFILE ID= $PROFILE ID" Quick unit test Test tab → Event name: test1 → Event JSON : { "queryStringParameters": { "model": "cheap", "prompt": "Please introduce yourself in three lines" } } Create an HTTP API name it bedrock-router-api with a /chat route integrated with the Lambda, then: Change the URL below to match your environment export API URL="https://abc123xyz.execute-api.us-east-1.amazonaws.com" echo "$API URL/chat" Route to each model by key: Call Claude Haiku fast curl -s -G "$API URL/chat" -d "model=fast" --data-urlencode "prompt=What is generative AI, in three lines" | jq Call Nova Micro cheap curl -s -G "$API URL/chat" -d "model=cheap" --data-urlencode "prompt=What is generative AI, in three lines" | jq Call Llama open curl -s -G "$API URL/chat" -d "model=open" --data-urlencode "prompt=What is generative AI, in three lines" | jq If no key is specified, the default fast is used curl -s -G "$API URL/chat" --data-urlencode "prompt=Hello" | jq '.model key, .model id' Check error handling for an invalid key curl -s -G "$API URL/chat" -d "model=unknown" --data-urlencode "prompt=test" | jq Same endpoint, three different models, chosen by a query parameter. Now the payoff. Suppose Claude Haiku is overkill for the fast route and you want Nova Micro there too. Create version 2 of the flags — note fast now carries $n — and deploy it: REGION=us-east-1 jq -n \ --arg c "$CLAUDE MODEL" --arg n "$NOVA MODEL" --arg l "$LLAMA MODEL" \ '{ flags: { fast: {name:"fast", attributes:{model id:{constraints:{type:"string"}}}}, cheap: {name:"cheap", attributes:{model id:{constraints:{type:"string"}}}}, open: {name:"open", attributes:{model id:{constraints:{type:"string"}}}} }, values: { fast: {enabled:true, model id:$n}, cheap: {enabled:true, model id:$n}, open: {enabled:true, model id:$l} }, version: "1" }' /tmp/flags v2.json aws appconfig create-hosted-configuration-version --region $REGION \ --application-id $APP ID --configuration-profile-id $PROFILE ID \ --content-type "application/json" \ --content fileb:///tmp/flags v2.json \ /dev/null aws appconfig start-deployment --region $REGION \ --application-id $APP ID --environment-id $ENV ID \ --deployment-strategy-id AppConfig.AllAtOnce \ --configuration-profile-id $PROFILE ID \ --configuration-version 2 Wait for the cache TTL up to ~30 seconds , then: fast should now be Nova Micro curl -s -G "$API URL/chat" -d "model=fast" --data-urlencode "prompt=Introduce yourself" | jq '.model key, .model id' The Lambda never changed. No deploy, no cold start, no release process — the model behind fast is now a different one, and deploying version 1 again would roll it back just as fast. API Gateway look up the API ID and delete API ID=$ aws apigatewayv2 get-apis --region us-east-1 \ --query 'Items ?Name== bedrock-router-api .ApiId' --output text 2 /dev/null if -n "$API ID" && "$API ID" = "None" ; then aws apigatewayv2 delete-api --region us-east-1 --api-id "$API ID" fi Lambda aws lambda delete-function --region us-east-1 --function-name bedrock-router 2 /dev/null IAM role inline policy first, then the role itself aws iam delete-role-policy \ --role-name bedrock-router-role \ --policy-name bedrock-router-inline 2 /dev/null aws iam detach-role-policy \ --role-name bedrock-router-role \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2 /dev/null aws iam delete-role --role-name bedrock-router-role 2 /dev/null AppConfig child resources first if -n "$APP ID" ; then aws appconfig delete-environment --region us-east-1 \ --application-id "$APP ID" --environment-id "$ENV ID" 2 /dev/null aws appconfig delete-configuration-profile --region us-east-1 \ --application-id "$APP ID" --configuration-profile-id "$PROFILE ID" 2 /dev/null aws appconfig delete-application --region us-east-1 --application-id "$APP ID" 2 /dev/null fi Separating "which model" from "the code that calls it" is not a convenience — it's how you keep an LLM application operable in a market where models are replaced every few months. AppConfig gives that separation deployment strategies, version history, and rollback for free. If your Bedrock model IDs live in code today, I recommend trying this pattern in your own environment. About the author Maruchin Tech — 12x AWS Certified | Cloud & AI for manufacturing and supply chain AWS / Google Cloud / Azure | Udemy instructor 100K+ students 🎥 Video version of this hands-on: https://youtu.be/6k2lO4 fA7o https://youtu.be/6k2lO4 fA7o 📚 Full course — AWS Certified Generative AI Developer Professional AIP-C01 Exam Prep: https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/ https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/ 👨🏫 All my courses Udemy profile : https://www.udemy.com/user/maruchin-tech-2/ https://www.udemy.com/user/maruchin-tech-2/ 🎫 Monthly discount coupons: https://www.youtube.com/@MaruchinTech-cloud/posts https://www.youtube.com/@MaruchinTech-cloud/posts