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