cd /news/large-language-models/swap-your-llm-without-a-deploy-dynam… Β· home β€Ί topics β€Ί large-language-models β€Ί article
[ARTICLE Β· art-106872] src=dev.to β†— pub= topic=large-language-models verified=true sentiment=Β· neutral

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.

read8 min views2 publishedAug 22, 2026

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.

aws bedrock list-inference-profiles \
  --region us-east-1 \
  --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `haiku`)].inferenceProfileId' \
  --output table

aws bedrock list-inference-profiles \
  --region us-east-1 \
  --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `nova-micro`)].inferenceProfileId' \
  --output table

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

APP_ID=$(aws appconfig create-application --region $REGION \
  --name bedrock-router --query Id --output text)

ENV_ID=$(aws appconfig create-environment --region $REGION \
  --application-id $APP_ID --name dev --query Id --output text)

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)

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

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.)

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)

_cache = {"config": None, "token": None, "expires_at": 0}
CACHE_TTL_SEC = 30

def _load_config():
    now = time.time()

    if _cache["config"] is not None and now < _cache["expires_at"]:
        return _cache["config"]

    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:
        _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:

export API_URL="https://abc123xyz.execute-api.us-east-1.amazonaws.com"
echo "$API_URL/chat"

Route to each model by key:

curl -s -G "$API_URL/chat" -d "model=fast" --data-urlencode "prompt=What is generative AI, in three lines" | jq

curl -s -G "$API_URL/chat" -d "model=cheap" --data-urlencode "prompt=What is generative AI, in three lines" | jq

curl -s -G "$API_URL/chat" -d "model=open" --data-urlencode "prompt=What is generative AI, in three lines" | jq

curl -s -G "$API_URL/chat" --data-urlencode "prompt=Hello" | jq '.model_key, .model_id'

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:

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_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

aws lambda delete-function --region us-east-1 --function-name bedrock-router 2>/dev/null

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

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

πŸ“š 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/

πŸ‘¨πŸ« All my courses (Udemy profile):

https://www.udemy.com/user/maruchin-tech-2/

🎫 Monthly discount coupons:

https://www.youtube.com/@MaruchinTech-cloud/posts

── more in #large-language-models 4 stories Β· sorted by recency
── more on @aws appconfig 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/swap-your-llm-withou…] indexed:0 read:8min 2026-08-22 Β· β€”