Build a governed LLM gateway that lets internal developers use code agents (Claude Code,
Codex) against Amazon Bedrock through a single control point — enforcing identity,
per-user virtual keys, model/cost tiering, Bedrock Guardrails, managed web search, and tracing.
Supports two developer-auth modes: IAM Identity Center organization-instance SSO (`org-sso`,
permission sets + AWSReservedSSO roles) and `cognito-native` (an Amazon Cognito User Pool as the
sole identity source, Cognito User Pool Groups as teams) for environments where org-sso is not
usable — e.g. an IdC account instance, which cannot host the SAML app that IdC federation would
require. Generates AWS CDK (TypeScript): VPC + Aurora Serverless v2 + Bedrock Guardrail +
AgentCore Web Search gateway + LiteLLM on ECS Fargate + optional Langfuse + a token
service + an internet-facing, SG CIDR-restricted ALB edge (config-driven TLS via certMode: acm/http) + cross-region Mantle (GPT-5.x) VPC peering. Use when the user asks to
"build an LLM gateway", "gove
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Build a governed LLM gateway that lets internal developers use code agents (Claude Code,
Codex) against Amazon Bedrock through a single control point — enforcing identity,
per-user virtual keys, model/cost tiering, Bedrock Guardrails, managed web search, and tracing.
Supports two developer-auth modes: IAM Identity Center organization-instance SSO (`org-sso`,
permission sets + AWSReservedSSO roles) and `cognito-native` (an Amazon Cognito User Pool as the
sole identity source, Cognito User Pool Groups as teams) for environments where org-sso is not
usable — e.g. an IdC account instance, which cannot host the SAML app that IdC federation would
require. Generates AWS CDK (TypeScript): VPC + Aurora Serverless v2 + Bedrock Guardrail +
AgentCore Web Search gateway + LiteLLM on ECS Fargate + optional Langfuse + a token
service + an internet-facing, SG CIDR-restricted ALB edge (config-driven TLS via certMode: acm/http) + cross-region Mantle (GPT-5.x) VPC peering. Use when the user asks to
"build an LLM gateway", "govern Bedrock access for developers", "central proxy for Claude
Code / Codex", "LiteLLM on AWS", or "SSO/Cognito virtual keys for LLMs".
Generate a production-shaped code-agent governance gateway on AWS: a single LiteLLM proxy
(on ECS Fargate) that internal developers reach with per-user virtual keys minted from a
verified identity, with Bedrock Guardrails, model/cost tiering, managed web search
(AgentCore), network isolation, and observability. The solution supports two
developer-auth modes: org-sso for IAM Identity Center organization instances using
permission-set roles, and cognito-native for environments where org-sso is not usable — an
Amazon Cognito User Pool is the sole identity source (no external IdP, no IdC federation) and
native Cognito User Pool Groups are the teams. cognito-native is the correct choice for an IdC
account instance (a member/standalone account whose IdC cannot host a SAML customer-managed
application, so IdC-federated login is impossible at the AWS level) or any account with no usable
IdC at all. It lets an org give developers Claude Code / Codex access to Amazon Bedrock while
centrally controlling who, which models, how much, what content, and with what audit
trail — without requiring LiteLLM Enterprise.
This skill produces AWS CDK (TypeScript), Lambda (Python), the LiteLLM container config, and
developer-onboarding scripts, customized to the user's domain (custom domain or not, Langfuse or
not, which models, which tiers, which region, web search on/off).
Knowledge sources
Read these before generating. All real knowledge lives in shared/:
shared/reference/prerequisites.md — check before Phase 1: local tooling (Docker, Node, CDK CLI, AWS CLI), AWS account access/IAM, Bedrock model access, IdC readiness, custom-domain/Route53 needs — incl. the two image-build paths (local Docker default / CodeBuild when Docker can't run locally)
shared/reference/architecture.md — the 10-stack architecture, request lifecycle, and the "why"
shared/reference/sso-setup.md — IAM Identity Center organization instance Discovery + permission-set provisioning + generated config.sso block & AuthStack outputs
shared/reference/account-instance-setup.md — cognito-native setup for IdC account instances (and any account without usable org-sso): why IdC federation is impossible on an account instance (no SAML customer-managed app), Cognito User Pool as sole identity source, User Pool Groups as teams, cognito:groups claim → LiteLLM team, and cross-platform llmgw-login
shared/reference/litellm-admin-guide.md — post-deploy operations: Admin UI login, creating teams/users (org-sso permission set → team, cognito-native Cognito User Pool Group → team), checking logs/traces (LiteLLM UI, Langfuse, CloudWatch), applying per-team/per-key budgets, offboarding (revoke virtual keys — IdC/Cognito removal alone does NOT cut off access; see constraints.md → "Virtual-key lifetime ≠ SSO session")
shared/patterns/cdk-stacks.md — full CDK source for the platform stacks + interfaces + config validation
shared/patterns/mantle-peering.md — Bedrock Mantle in us-east-1 via cross-region VPC peering (MantleNetworkStack + MantlePeeringRoutesStack)
Workflow
Phase 1: Discovery (ask only what you don't know)
Prerequisites check — before asking anything else, confirm the operator has what shared/reference/prerequisites.md lists: Docker daemon running, Node/CDK CLI/AWS CLI v2 installed, deploy access to the target account, Bedrock model access requested (gateway region for Claude, us-east-1 for GPT-5.x/Mantle), and IdC enabled (required for the SSO path — see #6). Surface any gap now rather than discovering it mid-deploy. Docker is the ONLY waivable item: if docker info succeeds, use the default local fromAsset() build; if Docker cannot run on this machine at all (e.g. managed Windows laptop — WSL2/Hyper-V install needs admin + a reboot) set litellm.imageBuild.mode='codebuild' (conditional ImageBuildStack builds the image on native ARM in CodeBuild — cdk-stacks.md §4-1, 3-step deploy order). Everything else in the local toolchain is a hard prerequisite — do NOT offer a remote/EC2 deploy host as a workaround: running the skill off the operator's machine breaks operator-locality (the albIngressCidrs Discovery answer is the operator's real egress IP, unknowable from a remote host; the generated onboarding bundle — including the secret-bearing admin-onboarding.html — lands on the remote host and needs yet another transfer channel to reach anyone).
Edge TLS (certMode)? CloudFront is removed — the ALB is the edge, always internet-facing, always SG CIDR-restricted. Choose: acm (own a domain + Route53 hosted zone → HTTPS:443 with a public ACM cert; ✅ recommended / PROD) or http (no domain → HTTP:80, no cert; ⛔ the virtual key and prompt/response bodies travel plaintext on the wire — PoC-only, a GATE-1 acknowledgement item). Both modes: ask which source CIDRs may reach the ALB (litellm.albIngressCidrs, required — the SG allowlist is the primary access control). 0.0.0.0/0 with http means the plaintext endpoint is reachable from the whole internet → its own explicit GATE-1 acknowledgement.
Models? Which Claude / GPT(Mantle) models? GPT tiers: gpt-5.6-sol (flagship coding/agentic, 1M context) / gpt-5.6-terra (balanced) / gpt-5.6-luna (economy/latency) / gpt-5.5 (proven flagship) / gpt-5.4 (proven economy). ⛔ If any gpt-5.6-* alias is selected, tell the user now that it carries a before developer onboarding (real-deploy incident history: the Codex tool-type 400 — since fixed server-side — plus open Codex-CLI issues like the ≥ 0.147 -namespace collision), and keep / in the model list as the fallback pair — see → the GPT-5.6 gate. ⚠️ () — a real deploy had and profiles but NO / at all; if absent, say so explicitly rather than offering a fallback that cannot be routed. : which teams need their own budget cap + model allowlist? In , each permission set maps 1:1 to a same-named LiteLLM team; in , each Cognito User Pool Group name maps 1:1 to a same-named LiteLLM team. Discovery should capture the team names plus initial / seeds. Onboarding additional teams later is then a console action (IdC permission set / Cognito group + LiteLLM Admin UI) only, never a code change. ("economy/standard" is just one illustrative naming — not a required split; name groups/permission sets/teams after real orgs/teams.)
⛔ GATE 1: summarize requirements + the resulting config/dev.json (incl. awsRegion, authMode, sso or cognitoNative — for cognito-native also the initial user(s) to create post-deploy, agentcore, mantle, litellm.certMode + litellm.albIngressCidrs — with the plaintext acknowledgement if http, and — if any Fable/Mythos-class model is requested — the provider_data_share opt-in acknowledgement from #2b below); await confirmation.
Phase 2: Architecture Design
Apply shared/reference/decision-tree.md to choose certMode (acm/http), enableLangfuse (acm only — Langfuse UI needs a real domain/cert; http → CloudWatch only), tiers, capacity, region, web search, Mantle peering.
Verify model IDs + regional availability via AWS Knowledge MCP (aws___search_documentation, aws___get_regional_availability) — never hard-code stale IDs. Resolve each Claude model's actual inference-profile ID with aws bedrock list-inference-profiles; do NOT assume a us. prefix. Recent (2026) models (Opus 4.8, Sonnet 5, Haiku 4.5, Fable 5) exist only as global. GLOBAL profiles — a us. call returns The provided model identifier is invalid. Confirm Web Search + Mantle PrivateLink in us-east-1.
Produce the stack list (Network → Data → Guardrail → AgentCoreGateway(us-east-1) → LiteLLM [ALB edge per certMode] → Langfuse? [acm only] → Auth(org-sso API Gateway IAM authorizer or cognito-native Cognito User Pools authorizer) → Observability → MantleNetwork(us-east-1) → MantlePeeringRoutes) and a cost estimate. There is no CdnStack — CloudFront is removed; the ALB is the edge.
Token Lambda auth adapter: generate a clear branch by authMode. org-sso preserves API Gateway IAM + AWSReservedSSO_ ARN parsing. cognito-native uses an API Gateway Cognito User Pools authorizer (which validates the JWT signature/issuer/audience/expiry before the Lambda runs) and reads the verified cognito:groups claim from requestContext.authorizer.claims — no Identity Store lookup, no identitystore:* IAM at all. Never mix the trust anchors.
Token Lambda tiering (Hard Constraints #13): generate _resolve_team_id so the authorization unit maps 1:1, unbranched, to a same-named LiteLLM team — permission set for org-sso, Cognito User Pool Group name for cognito-native; never emit an if team in {...} per-org branch. Seed TIER_CONFIG only with the team(s) named in Discovery (as a starter models/max_budget for that team's first-ever creation); this is what keeps future onboarding console-only (permission set / Cognito group + LiteLLM Admin UI), not a Lambda redeploy. See shared/patterns/lambda-handlers.md.
No CdnStack (CloudFront removed). Emit the LiteLLM ALB edge per certMode (Hard Constraints #1/#5): →internet-facing HTTPS:443 + ACM (regional — , ARN or Route53 DNS-issued) + Route53 alias + HTTP→443 redirect; →internet-facing HTTP:80 (no cert). In modes the public ALB's SG ingress is restricted to (no AWS WAF, no Custom Resource, no SSM tunnel). Always keep a separate for the Token Service (SSM URL unchanged). ALB (Hard Constraints #10). Langfuse (acm only) gets its own public ALB.
Phase 4: Validate
Toolchain alignment first: aws-cdk-lib + cdk-nag must be co-resolved (an old lib pin + open nag range → ERESOLVE), and the aws-cdk CLI must support the library's cloud-assembly schema (schema version mismatch at synth means: upgrade the CLI to at least the version the error names). See constraints.md.
Pinned-image CLI pre-check: verify every flag the entrypoint passes against the pinned LiteLLM image — docker run --rm --entrypoint litellm <image> --help (seconds; skipping this cost a full deploy-rollback cycle in a real deploy: --forwarded-allow-ips does not exist → exitCode 2 at boot).
Resolve cdk-nag findings: suppress with written justification (PROD TODO) where they are intentional dev tradeoffs (e.g., IAM5, ELB2, EC23 — no CFR* findings exist since CloudFront is removed), fix genuine issues.
Verify data.engineVersion exists in the target region; confirm mantle.peerVpcCidr does not overlap network.vpcCidr.
Phase 5: Deploy
Ensure Docker is running (LiteLLM image builds via fromAsset). ⚠️ On an x86_64 deploy host (Windows, Intel Mac, x86 CI), the generated CDK pins the build to Platform.LINUX_ARM64 to match the ARM64/Graviton Fargate task — but that cross-build needs QEMU emulation (Docker Desktop has it built in; native Linux Docker needs docker run --privileged --rm tonistiigi/binfmt --install arm64 once). If a generated project's ContainerImage.fromAsset() is missing the platform option, the build silently produces an amd64 image and cdk deploy still succeeds — the failure only surfaces afterward as ECS tasks crash-looping with exec format error. See shared/reference/constraints.md → "Docker build architecture mismatch on x86 hosts". imageBuild.mode='codebuild' deploys instead follow the 3-step order (cdk deploy --all alone ships no image — StartBuild is not a CloudFormation resource): deploy ImageBuildStack → aws codebuild start-build + poll to SUCCEEDED + aws ecr describe-images confirms the tag → deploy the remaining stacks (cdk-stacks.md §4-1; skipping step 2 surfaces late as CannotPullContainerError).
Bootstrap us-east-1 AND the gateway region: cdk bootstrap aws://<acct>/<awsRegion> aws://<acct>/us-east-1 — if leftovers block it, use a custom qualifier (Hard Constraints #2). ⚠️ "no changes" is not proof of a correct bootstrap: the account may be bootstrapped under a different qualifier — check aws cloudformation describe-stacks --stack-name CDKToolkit → the Qualifier parameter, and set @aws-cdk/core:bootstrapQualifier in cdk.json to match (else deploy fails assuming cdk-hnb659fds-* roles that don't exist).
cdk deploy --all --require-approval never --outputs-file outputs.json. ⚠️ A subset deploy must NOT write the canonical outputs.json — cdk deploy <one-stack> --outputs-file outputs.json OVERWRITES the file with only that stack's outputs, silently deleting every other stack's (real-deploy incident: a missing Cognito host produced https:///oauth2/authorize far downstream). Use --outputs-file /tmp/<stack>-outputs.json for subset deploys, and repopulate the canonical file with . — the running deploy keeps using the image built from the old source; wait for it to finish (or cancel), then start a fresh deploy. A container dying in seconds (exitCode N) → check , not ECS events.
Phase 6: Developer Onboarding — ALWAYS end by generating the two HTML docs
After a successful deploy you MUST generate two self-contained HTML docs with scripts/gen-onboarding.py (from cdk deploy --outputs-file outputs.json), per shared/patterns/developer-onboarding.md §6. This replaces the old inline markdown guide. The base URL is the GatewayUrl output (the ALB domain — CloudFront is removed).
developer-setup.html — hand to every developer. No admin secrets.Script-first: the doc instructs developers to RUN the generated .sh/.ps1 launchers — never to hand-edit config files (the merged JSON/TOML content appears only as a reference appendix). Filled with deployed values:
Step 1 — one-shot setup: run scripts/setup-developer.sh (Windows: scripts/setup-developer.ps1). It merges ~/.claude/settings.json (ANTHROPIC_BASE_URL=<gateway-url>, AWS_REGION=<deploy-region>, all four model aliases incl. ANTHROPIC_DEFAULT_FABLE_MODEL — omitting Fable hides that tier from /model —, apiKeyHelper → the token helper, permissions.deny: ["WebSearch"] — use the AgentCore Web Search MCP instead of the unsupported built-in) and ~/.codex/config.toml ([model_providers.llm-gateway]: base_url=<gateway-url>/v1, wire_api=responses, model=<gpt-alias>, web_search="disabled", .auth.command → the token helper), backing both up to *.llmgw-backup-<timestamp> first (merge, never overwrite).
Step 2 — login: org-sso → aws sso login --profile llm-gateway (Token Service accepts only AWSReservedSSO_ principals; a non-SSO caller is rejected 403 by design). cognito-native → llmgw-login / llmgw-login.ps1 (Cognito Hosted UI; aws sso login is NOT used).
http only: the base URL is http://<alb-dns> (plaintext — reachable only from the SG albIngressCidrs allowlist; no cert trust, no tunnel).
Step 3 — web search MCP (client-side, required): claude mcp add-json at <gateway-url>/mcp/ with headersHelper → gateway_auth.py mcp-headers (rotating virtual key injected dynamically). Tool: websearch-web-search-tool___WebSearch.
Hard Constraints
The ALB is the edge — TLS via certMode, CloudFront removed. The ALB is always internet-facing and always SG CIDR-restricted (litellm.albIngressCidrs). acm needs a domain (regional public ACM cert — an existing ARN, or Route53 DNS-issued) → HTTPS:443. http needs no domain → HTTP:80, ⛔ plaintext on the wire, PoC-only (GATE-1 acknowledgement). There is no self-signed mode, no CloudFront, no internal/VPN exposure variant, no SSM tunnel. (See constraints.md.)
Bootstrap collisions → bootstrap with a custom --qualifier + @aws-cdk/core:bootstrapQualifier in cdk.json; delete any empty REVIEW_IN_PROGRESSCDKToolkit stack. Bootstrap both us-east-1 and the gateway region. Never delete other apps' bootstrap resources.
Bedrock Guardrails are bedrock-runtime only — never attach them to bedrock_mantle/ (GPT) models; cover Mantle with LiteLLM hide-secrets and document the gap.
Never hard-code secrets. Master key, DB creds, Langfuse admin password + project keys all go through Secrets Manager. Shared LiteLLM↔Langfuse trace keys must live in a stack created before LiteLLM and be injected with ecs.Secret.fromSecretsManager — never as plaintext environment values.
The public ALB is the edge (internet-facing in both modes: acm HTTPS:443, http HTTP:80) — access control is the albIngressCidrs SG allowlist (no AWS WAF; mention WAF only as an optional PROD hardening note for a fully open acm deployment). Always keep a separate internal ALB (:4000) for the Token Service (its SSM URL is unchanged, no hairpin). Langfuse (acm only) has its own public ALB. Never expose the internal ALB or the :4000 listener to the internet. ⚠️ Pass open: false on EVERY addListener() — CDK's default open: true silently appends a 0.0.0.0/0 ingress rule for the listener port to the ALB SG, defeating albIngressCidrs (real-deploy incident; synth-time invisible). After any deploy touching listeners, verify the deployed SG () contains only the intended CIDRs. See .
Generation rules
Single source of truth in lib/config/constants.ts; runtime-validate config/dev.json (lib/config/schema.ts, incl. awsRegion/authMode/sso/cognitoNative/agentcore/mantle) so deploy fails fast.
Append-only cross-stack *Exports interfaces; runtime-only wiring via SSM by name; cross-region wiring via crossRegionReferences: true.
SG ownership rule (cyclic-reference, real-deploy lesson): NetworkStack owns only shared-infra SGs (Aurora/endpoints/Lambda) with CIDR-based ingress; app stacks (LiteLLM/Langfuse) own their service + ALB SGs. Never let a Network-owned SG hold a rule referencing an app-stack SG — and remember addTargets() auto-wires SG rules. See constraints.md + cdk-stacks.md §1.
Onboarding scripts merge, never overwrite, the user's shared config files (~/.claude/settings.json, ~/.codex/config.toml): backup + JSON load-update-save / TOML block-replace only (constraints.md). The merge logic exists once, in gateway_auth.py setup — .sh/.ps1 are thin launchers; never re-implement it in shell/PowerShell. The merge also removes direct-Bedrock remnants (CLAUDE_CODE_USE_BEDROCK, AWS_BEARER_TOKEN_BEDROCK, top-level model) — leaving them causes 403/"team not allowed to access model" (real Windows incidents, constraints.md). Keep :? error messages apostrophe-free (macOS bash 3.2).
Windows client rules (constraints.md → "Cross-platform client onboarding"): every .ps1 launcher ends with exit $LASTEXITCODE (PS 5.1 does not propagate native exit codes) and runs the Python-3 preflight — probe py -3 then python with --version, accept only a real Python 3.x banner + exit 0, else an actionable stderr error (winget install Python.Python.3.12) + exit 1; preferring py -3 alone is not enough — on a clean box is absent AND is the Store-alias stub (silent exit 9009 = "script does nothing", real incident); config helper commands use + the copy that installs; (no shell; joined string → os error 123 → 401); user-only file protection needs (chmod is a no-op on Windows).
Confirm the fallback pair actually exists in this account/region before promising it
aws bedrock list-inference-profiles
gpt-5.6-*
gpt-oss-*
gpt-5.5
gpt-5.4
Per-team governance (optional)
org-sso
cognito-native
models
max_budget
2b. Fable/Mythos-class data-retention opt-in (GATE-blocking). If any requested model is a Fable/Mythos-class model (e.g. claude-fable-5), it is restricted to allowed_modes: ["provider_data_share"] — the account (per-region) data-retention mode must be set to provider_data_share or the model is blocked outright. Opting in means prompts/responses to that model may be retained by Anthropic for 30 days and subject to human safety review. This is a policy decision, so it must be surfaced at GATE 1 and explicitly approved by the account owner — never assume it. The opt-in is per-region and set only via the Bedrock control-plane REST API (no console UI). See shared/reference/constraints.md.
Observability? Langfuse (prompt/trace level) on, or CloudWatch only? Either way, the CloudWatch usage dashboard (ObservabilityStack, dashboardEnabled) ships by default: token usage by model & team, spend, latency, failures, per-user top-N and hourly-activity tables (Logs Insights over the cloudwatch_usage EMF records) — so per-user token accounting exists even without Langfuse. Langfuse adds the prompt/trace level on top.
Region & account? Target gateway region (config.awsRegion, authoritative). AgentCore Web Search, CDN, and Mantle are pinned to us-east-1 — so confirm Claude access in the gateway region and GPT-5.x (Mantle) + Web Search access in us-east-1.
Web search? Use the managed AgentCore Web Search Tool (built-in web-search connector on an AgentCore Gateway, us-east-1)? Or no web search? (Tavily/3rd-party API keys are no longer used.)
Identity and authMode? Decide between org-sso and cognito-native before choosing the auth path. Detect IdC state first when possible:
aws sso-admin list-instances --region <idc-region> → capture InstanceArn, IdentityStoreId, OwnerAccountId (empty result = no IdC in this region).
aws organizations describe-organization → if available, compare the management account to OwnerAccountId.
OwnerAccountId == management account (organization instance) → authMode="org-sso" is available.
Account instance (OwnerAccountId != management account, or a standalone account instance), or no usable IdC at all → use authMode="cognito-native". ⚠️ Do not attempt account-sso/IdC federation here: an IdC account instance cannot host a SAML 2.0 customer-managed application (AWS-confirmed), so Cognito↔IdC SAML federation is impossible at the AWS level. Its only customer-managed app type is OAuth 2.0 for trusted identity propagation, which is the inverse direction and cannot serve as a login/IdP. Never force an account instance down the permission-set or SAML-federation path.
Helper signal: org-sso relies on permission sets; cognito-native uses none. When the partner/payer owns the org IdC and you only have an account instance, cognito-native is the answer.
If authMode="org-sso": Is IdC enabled + in which region? Identity source (IdC directory vs external IdP)? Permission set: create a NEW one for this gateway or reuse an existing one — and what name? (Default to creating a new, uniquely-named one; a name match like LlmGatewayUser is NOT proof of ownership — never silently reuse/edit a pre-existing permission set, as it may belong to other groups/another gateway.) Which group(s) or users to assign? Optional tier mapping. These populate config.sso. See shared/reference/sso-setup.md.
If authMode="cognito-native": Do not use aws sso login, permission sets, or IdC/Identity Store at all — the Cognito User Pool is the sole identity source. Ask for/plan: team → Cognito User Pool Group names (each group name IS the LiteLLM team, 1:1), teamGroupPrefix (recommended llmgw-) to scope which groups count as teams, multiGroupStrategy (require-single-team-group), and optional passwordMinLength / refreshTokenValidityDays. Also ask for the initial user(s) — email + the team group to assign — because the AuthStack creates the pool and groups but zero users, and the full-path verification (login → token → virtual key) needs at least one group-assigned user; the agent creates them right after deploy (Phase 5). These populate config.cognitoNative. See shared/reference/account-instance-setup.md.
acm
config.awsRegion
http
both
litellm.albIngressCidrs
SelfSignedCert
internal ALB (:4000)
idleTimeout=900s
Wire web search via the AgentCore Gateway (Hard Constraints #11) and Mantle via cross-region peering (Hard Constraints #12).
Mantle (GPT-5.x) auth is Bearer-token (Hard Constraints #6): the LiteLLM image installs aws-bedrock-token-generator (via uv, since the base image has no pip — re-verified on v1.98.0), and a mantle_token_refresh callback mints a short-term Bedrock API key from the Task Role into BEDROCK_MANTLE_API_KEY (never AWS_BEARER_TOKEN_BEDROCK). Emit services/litellm/callbacks/mantle_token_refresh.py.
Wire secrets through Secrets Manager — never hard-code credentials (Hard Constraints #4). Langfuse trace keys are injected via ecs.Secret.fromSecretsManager, never as plaintext environment values.
If a fix is needed mid-deploy: never edit source while a deploy is still running/rolling back
CloudWatch Logs first
Run developer onboarding automatically — do not ask the operator to assemble env vars or run scripts by hand. Execute scripts/setup-developer.sh yourself (Windows operator machine: scripts\setup-developer.ps1 or python scripts\gateway_auth.py setup — the .sh needs bash/WSL); both are thin wrappers over gateway_auth.py setup, the single cross-platform implementation. It is zero-touch: derives the gateway URL (scheme+host from the GatewayUrl output — the ALB domain; CloudFront is removed), Token Service URL, auth mode, and (org-sso) SSO profile values from outputs.json. For cognito-native, the same setup writes the llmgw-login config (Cognito Hosted UI domain, app client id, issuer, token endpoint). For http, the base URL is http://<alb-dns> — reachable only from the albIngressCidrs allowlist; no cert trust or tunnel setup exists.
SSO provisioning (org-sso only): per the Phase 1 decision, create the permission set(s) — name with no underscore, prefer a new uniquely-named one (do NOT reuse a pre-existing permission set just because the name matches) — with an execute-api:Invoke-only inline policy whose Resource is arn:aws:execute-api:<config.awsRegion>:<account>:<tokenServiceApiId>/* (region + API id MUST match the deployed Token Service, else every SSO call 403s), assign to the account (the user-specified group(s) or users), provision-permission-set, and hand off password activation (IdC console only). Follow shared/reference/sso-setup.md.
cognito-native provisioning (account instance / no-IdC only): do not create permission sets or any IdC/Identity Store resource. The AuthStack already creates the Cognito User Pool, Hosted UI domain, app client (Authorization Code + PKCE, loopback redirect), and the User Pool Groups that are the teams — but zero users. The agent then creates the initial user(s) from the Phase 1 answer itself: aws cognito-idp admin-create-user --user-pool-id <pool> --username <email> --user-attributes Name=email,Value=<email> --desired-delivery-mediums EMAIL + aws cognito-idp admin-add-user-to-group --group-name <llmgw-team> (optionally admin-set-user-password --permanent if the operator supplies a password for immediate activation instead of the email invite). Then run the full-path verification with that user: llmgw-login → access token → Token Service → virtual key → /v1/models — a master-key test alone proves nothing about this path. Ongoing on/offboarding stays Cognito console/CLI only. Follow shared/reference/account-instance-setup.md.
Fable/Mythos data-retention opt-in (if approved at GATE 1): before the first call to a Fable/Mythos-class model, set the account data-retention mode to provider_data_share, in every region the model is invoked from (the gateway region — and it is per-region, so a setting in one region does not cover another). Follow the order in shared/reference/constraints.md: ① read first — aws bedrock get-account-data-retention (already provider_data_share → nothing to set); ② set with the plain CLI — aws bedrock put-account-data-retention; ③ only if the CLI lacks the subcommand (older toolchains) fall back to the raw SigV4-signed PUT /data-retention (NOT /account-data-retention; the name-guessed path 404s). Do not jump straight to the SigV4 bypass. Without the opt-in the model returns access-denied.
Mantle warm-up: after a fresh-account deploy, make one call per GPT-5.x model to trigger the Marketplace auto-subscribe (first call may transiently 5xx for ~1 min). Use max_tokens ≥ 16 in the warm-up payload — the Mantle (OpenAI Responses) route rejects smaller values with integer_below_min_value (a max_tokens: 10 probe fails; Claude routes have no such floor — see constraints.md).
⛔ GPT-5.6 smoke test (only if a gpt-5.6-* alias is deployed — MUST pass before Phase 6): run a fresh multi-turn, tool-using Codex session against each 5.6 alias with top-level web_search = "disabled" and the default MCP servers registered; confirm in CloudWatch Logs that the session's /v1/responses calls return 200 (any namespace/unknown variantvalidation_error = the known incident pattern). If a 5.6 alias is exposed to Claude Code, run one Claude Code session against it too. On failure: de-alias the failing 5.6 entry, ship on gpt-5.5/gpt-5.4, and record the Codex CLI version + error signature. See constraints.md → the GPT-5.6 gate.
Usage-dashboard check: after the verification calls, confirm the LiteLLM log group has a "llmgw": "usage" EMF line and aws cloudwatch list-metrics --namespace <METRICS.NAMESPACE> shows TotalTokens within ~2 min; then open the DashboardUrl output — the token/user widgets should populate. Log line present but no metric = malformed EMF (multi-line record or empty dimension value); see litellm-gateway.md §4. Before any of the above verification calls, confirm the ECS service actually reached steady state — cdk deploy returning success only means the CloudFormation stack update completed, not that the container is running: aws ecs describe-services --cluster <cluster> --services <service> --query 'services[0].deployments[0].{running:runningCount,desired:desiredCount,rolloutState:rolloutState}' should show rolloutState: COMPLETED with running == desired. If it is stuck IN_PROGRESS or rolled back, check CloudWatch Logs for exec format error first (Docker build/task architecture mismatch — see constraints.md) before assuming an application bug.
Step 4 — verify: run scripts/healthcheck.sh / .ps1 (token helper → GET /v1/models; expect the aliases incl. GPT-5.x — plus GET /v1/mcp/tools for the websearch tool). Region is never hardcoded (the helper derives the SigV4 region from the Token Service URL host / Cognito issuer); note the cognito-native id-token gotcha (send the id_token, not the access token — only the id_token carries the email claim logged as the LiteLLM user_id; the authorizer has no authorizationScopes so it accepts it) and Windows PowerShell launchers — as detailed in developer-onboarding.md.
admin-onboarding.html — operator ONLY (written 0600; embeds the LiteLLM master key + Langfuse admin password). Add it to .gitignore; never share/commit. Contains: deployed endpoints + identity, secret values, password-change (master key → Secrets Manager put-secret-value + ecs update-service --force-new-deployment; Langfuse via its UI; Cognito admin-set-user-password), developer on/offboarding (admin-create-user + admin-add-user-to-group; admin-disable-user/admin-delete-user/admin-remove-user-from-group + revoke the LiteLLM virtual key + the DynamoDB key-cache TTL caveat), and a quick admin test (master key as bearer vs <gateway-url>/v1).
Hand the operator shared/reference/litellm-admin-guide.md for ongoing ops (LiteLLM Admin UI /ui/ login with the master key, team/budget management, request logs/traces via the LiteLLM UI · Langfuse [acm only] · CloudWatch). This is what the operator needs after the first developer is onboarded, distinct from the one-time deploy steps above.
aws ec2 describe-security-groups
constraints.md
Mixed model auth — Claude is tokenless SigV4, Mantle (GPT-5.x) is Bearer-token. Claude (bedrock/) authenticates via the ECS Task Role (SigV4) — nothing to rotate. Mantle (bedrock_mantle/) on the pinned v1.98.0 image uses a Bearer when present and only otherwise a SigV4 fallback that signs service "bedrock" — disputed upstream (BerriAI/litellm#31475) and not relied on (verified by extracting the actual installed source from the pinned image; the retired v1.89.0-rc.1 pin had no SigV4 path at all). The image therefore installs aws-bedrock-token-generator and a mantle_token_refresh LiteLLM callback mints a short-term Bedrock API key from the Task Role's own credentials and keeps it fresh in-process (no long-term IAM user, no static secret, no external scheduler) — a present Bearer takes precedence, keeping auth deterministic. ⚠️ The token MUST be written to BEDROCK_MANTLE_API_KEY, never AWS_BEARER_TOKEN_BEDROCK. The latter is a boto3-reserved name: set it and everybedrock-runtime client in the process (including Claude's SigV4 calls) switches to Bearer auth and Claude breaks with 403 (this exact mistake caused a 4-model outage). Before shipping any change to Mantle auth, run a 3-step regression: Claude call → GPT call (triggers callback) → Claude call again. See shared/patterns/litellm-gateway.md.
Production posture — removalPolicy: RETAIN + backups, per-AZ NAT, scoped IAM (no */bedrock-mantle:*), access/flow logs. Dev sample uses the opposite; tag each with PROD TODO.
UIs must not redirect to a dead host — and NEVER pass --forwarded-allow-ips (LiteLLM Admin UI + Langfuse). The pinned LiteLLM image's CLI does not have--forwarded-allow-ips (nor a FORWARDED_ALLOW_IPS env fallback — verified against the actual image); passing it kills the container at boot (No such option, exitCode 2 → circuit-breaker rollback; a real-deploy incident). Redirect correctness comes from PROXY_BASE_URL: for acm, set it to the gateway URL (https://<custom-domain>, known at synth); for http, leave it empty (the ALB DNS is not known when the container definition is synthesized) — the /ui→/ui/ 307 scheme quirk is cosmetic only. NEXTAUTH_URL for Langfuse = its acm domain. Verify any doc-sourced CLI flag against the pinned image (docker run --rm --entrypoint litellm <image> --help) before emitting the entrypoint. See constraints.md.
Auth mode is explicit — org-sso or cognito-native. Default authMode="org-sso" preserves the organization-instance path: Token Service accepts only AWSReservedSSO_ principals, and provisioning creates a no-underscore permission set with execute-api:Invoke on the Token Service API. For an IAM Identity Center account instance or any account without usable org-sso, use authMode="cognito-native": an Amazon Cognito User Pool is the sole identity source (no external IdP, no IdC federation, no Identity Store), the Token Service sits behind a Cognito User Pools authorizer, and it reads the verified cognito:groups claim to resolve the team. ⚠️ Do not attempt IdC↔Cognito SAML federation on an account instance — account instances cannot host a SAML customer-managed application (AWS-confirmed), so that path is impossible. (account-sso remains in the schema only as a deprecated no-op for old config snapshots; do not generate it.) See shared/reference/sso-setup.md and shared/reference/account-instance-setup.md.
Region is config-driven — config.awsRegion is authoritative (bin/app.ts: config.awsRegion ?? CDK_DEFAULT_REGION ?? AWS_REGION); never require editing constants.ts. AgentCoreGateway and MantleNetwork are pinned to us-east-1 (bootstrap us-east-1 and the gateway region). Long completions are governed by the ALB idleTimeout (config.litellm.albIdleTimeoutSeconds, default 900s, max 4000s) — the old CloudFront hard 120s VPC-Origin ceiling that 504'd Opus/Fable extended thinking (with no LiteLLM access-log line) is gone. ACM certs for the ALB are regional (config.awsRegion), not the former us-east-1 CloudFront cert.
Web search = AgentCore Web Search Tool — provision AWS::BedrockAgentCore::Gateway (MCP, AWS_IAM inbound) + GatewayTarget (connectorId: web-search, GATEWAY_IAM_ROLE) in us-east-1; LiteLLM calls it with SigV4 (bedrock-agentcore:InvokeGateway). No Tavily, no 3rd-party API key. ⚠️ The com.amazonaws.<region>.bedrock-agentcore VPC endpoint MUST be privateDnsEnabled: false — true makes a Private Hosted Zone authoritative for the whole bedrock-agentcore.<region>.amazonaws.com zone, so the per-gateway subdomain LiteLLM actually calls NXDOMAINs ([Errno -2] Name or service not known, MCP unhealthy) — real-deploy incident. See shared/patterns/agentcore-websearch.md.
Mantle (GPT-5.x) = us-east-1 via cross-region VPC peering — MantleNetworkStack (peer VPC + bedrock-mantle endpoint + peering + acceptance custom resource + cross-region PHZ) + MantlePeeringRoutesStack (primary-side routes). Pin the region via the vars the provider actually reads: per-model aws_region_name + env BEDROCK_MANTLE_REGION=us-east-1 + BEDROCK_MANTLE_API_BASE=https://bedrock-mantle.us-east-1.api.aws (MANTLE_REGION is NOT read by LiteLLM — doc alias only). Task Role needs aws-marketplace:Subscribe (first-call auto-subscribe). mantle.peerVpcCidr must not overlap network.vpcCidr. See shared/patterns/mantle-peering.md.
Authorization unit = LiteLLM team_alias, unbranched. In org-sso, the permission set name maps 1:1 to a same-named LiteLLM team. In cognito-native, the Cognito User Pool Group name maps 1:1 to a same-named LiteLLM team (filtered by teamGroupPrefix; require-single-team-group rejects a caller with zero or ambiguous matching groups). Never hard-code an if team in {...} org/tier list in handler.py. Onboarding a new team after deploy must be achievable via a permission set / Cognito group (+ membership) + LiteLLM Admin UI team settings, with no Lambda code change or redeploy. TIER_CONFIG may seed a team's first-ever creation with initial models/max_budget from Discovery answers, but is never consulted again once the team exists. See shared/patterns/lambda-handlers.md.
Verify model IDs, never assume a us. prefix. Resolve each Claude model's actual inference-profile ID with aws bedrock list-inference-profiles at Design time. Recent (2026) models (Opus 4.8, Sonnet 5, Haiku 4.5, Fable 5) are published only as global. GLOBAL profiles — bedrock/global.anthropic.<model-id>; a us. call returns The provided model identifier is invalid. IAM must grant the GLOBAL profile ARN plus the unqualified arn:aws:bedrock:::foundation-model/<model> fan-out target (and the gateway-region foundation-model ARN), not us-east-1/us-west-2 regional ARNs.
Fable/Mythos-class models need provider_data_share opt-in. These models are restricted to allowed_modes: ["provider_data_share"]; the account data-retention mode must be set to provider_data_shareper region (Bedrock control-plane REST API, no console UI) or calls are blocked. This permits 30-day Anthropic retention + human review, so it is a GATE-1 approval item, never assumed. See shared/reference/constraints.md.
Do not plant a boto3-reserved env var name. Before setting any process-global env var that an SDK might special-case, check it is not reserved. Specifically, the Mantle token goes in BEDROCK_MANTLE_API_KEY, never AWS_BEARER_TOKEN_BEDROCK (boto3 intercepts the latter for all bedrock-runtime clients and breaks Claude's SigV4). In a shared-process gateway, always regression-test the providers you did not change. See Hard Constraint #6.
http certMode = plaintext, PoC-only, SG-locked. The virtual key and prompt/response bodies travel unencrypted from client to ALB. The deploy MUST restrict the public ALB SG to known source CIDRs (litellm.albIngressCidrs — a required Discovery answer, not a synth-time guess); 0.0.0.0/0 means the plaintext endpoint is reachable from the whole internet and requires its own explicit GATE-1 acknowledgement. Prefer acm whenever a domain is available or the traffic is sensitive.
Langfuse UI requires certMode='acm'.enableLangfuse=true is valid only with a domain (public ALB + ACM); http deploys are CloudWatch-only (schema fail-fast). When acm, whether to deploy Langfuse is a free enableLangfuse choice exactly as before.
Any deployed gpt-5.6-* alias MUST pass the post-deploy Codex smoke test before Phase 6 onboarding.constants.tsMODELS defines GPT56_SOL/GPT56_TERRA/GPT56_LUNA alongside GPT55/GPT54; config.yaml routes the tiers selected at Discovery. GPT-5.6 is supported on the current v1.98.0 pin (LiteLLM ships the 5.6 routes since v1.93.0; the old chat-completions-fallthrough failure is gone), but it has real-deploy incident history (the Codex namespace 400 — fixed server-side 2026-07-19) and open Codex-CLI issues (≥ 0.147 functions-namespace collision, BerriAI/litellm#36545) — so a fresh multi-turn, tool-using Codex session per 5.6 alias (web_search="disabled") is mandatory, with gpt-5.5/gpt-5.4 kept as the fallback pair only if aws bedrock list-inference-profiles shows they exist in this account/region — do not wire a nonexistent profile ID into the active model_list (defining it in constants.ts for forward-compatibility is fine; routing to it fails every call). On an already-deployed environment, adding 5.6 also requires updating existing teams' models allowlists (Admin UI / /team/update — TIER_CONFIG seeds first creation only, #13). See shared/reference/constraints.md → the GPT-5.6 gate.
py
python
sys.executable
~/.llm-gateway/gateway_auth.py
setup
Codex auth must be command (executable only) + args (array) — never one joined string
icacls
ARM64/Graviton for all Fargate/Lambda.
Every cdk-nag suppression carries a written reason.