An OpenAI-compatible HTTP gateway for Amazon Bedrock: Chat Completions, Responses, Completions and Embeddings from one Rust binary.
🌐 Website: firlab.app/bedrock-gateway · 📖 中文文档 / Chinese README
Point any OpenAI SDK, tool, or agent at this gateway and it routes requests to AWS Bedrock without a single line of client-side changes. The runtime is axum + tokio + aws-sdk-bedrockruntime — fully async, no GC pauses, no Python dependency. It replaces an earlier Python/FastAPI implementation while preserving wire-exact OpenAI API compatibility.
- 100% Rust — single statically-linked binary, distroless Docker image (~12 MB compressed), no GC, low memory footprint, high concurrency under load.
- Wire-exact OpenAI compatibility — existing OpenAI SDKs, clients, and agents connect with zero code changes. No invented top-level fields; Bedrock-only features go through the standard
extra_bodymechanism. - OpenAI Responses API — full support for
POST /api/v1/responses, including streaming. Required bycodex(wire_api = "responses"). Stateless;storeandprevious_response_idare accepted and silently ignored. - Legacy text completions —
POST /api/v1/completionsfor editors like Zed edit-prediction; reuses the Converse path and returns thetext_completionshape (stream + non-stream). - Automatic prompt caching — cache-point injection is on by default. The gateway places cache points across tools, system prompt, and messages automatically (up to
max_cache_checkpointsper model). No client changes needed. Thresholds and limits are config-driven per model; a family fallback entry covers new Claude models automatically. - Extended thinking / reasoning — Claude
budget_tokens,adaptive_thinking, and DeepSeek string-form reasoning all supported. Map OpenAI'sreasoning_effortlevels (low/medium/high/xhigh/max) throughextra_body; the gateway picks the right Bedrock wire format per model. - Cross-region inference profiles — all seven geographic prefixes (
us./eu./apac./jp./au./ca./global.) work transparently. Capability matching strips the prefix; Bedrock calls always use the original model ID. - Zero-hardcoding — all model knowledge lives in
config/models.toml. Adding a new model or tuning cache thresholds never requires a recompile. - Four deployment targets — standalone binary, Docker, ECS/Fargate + ALB (CloudFormation one-click), and Lambda + Function URL (Lambda Web Adapter, no Lambda-specific Rust code).
- Dual auth, dual direction — client-to-gateway via Bearer token (SSM / Secrets Manager / env); gateway-to-Bedrock via Bedrock API Key bearer or SigV4 credential chain.
- Structured observability — per-request
request_id,cached_tokens,cache_hit,ttfb_ms,duration_msin structured JSON logs. No prompt text or secrets ever logged.
All endpoints are prefixed by API_ROUTE_PREFIX (default /api/v1).
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/chat/completions |
Chat completions — streaming (SSE) and non-streaming |
POST |
/api/v1/completions |
Legacy text completions (text_completion) — for Zed edit-prediction; reuses the Converse path |
POST |
/api/v1/responses |
OpenAI Responses API — stateless, streaming and non-streaming |
POST |
/api/v1/embeddings |
Embeddings — Cohere, Titan, and Nova families |
GET |
/api/v1/models |
Live model catalog from Bedrock control plane |
GET |
/api/v1/models/{id} |
Single model lookup (supports inference profile IDs) |
GET |
/api/v1/health |
Liveness probe — returns 200 OK |
| Client | Wire API | Endpoint | Status |
|---|---|---|---|
| opencode | OpenAI Chat Completions | POST /api/v1/chat/completions |
✅ Supported |
| hermes | OpenAI Chat Completions | POST /api/v1/chat/completions |
✅ Supported |
| Zed (edit prediction) | OpenAI Legacy Completions | POST /api/v1/completions |
✅ Supported |
| codex | OpenAI Responses API | POST /api/v1/responses |
✅ Supported |
| claude code | Anthropic Messages | POST /v1/messages |
⏳ Roadmap |
- AWS credentials available (instance role,
AWS_PROFILE, or access key pair) - Bedrock model access enabled in your target region
- Docker, a release binary, or Rust (the version in
rust-toolchain.toml) to build from source
docker run \
-e API_KEY=sk-my-secret-key \
-e AWS_REGION=us-east-1 \
-e AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-... \
-p 8080:8080 \
sunerpy/bedrock-gateway-rustUsing an EC2 instance or ECS task with an IAM role? Omit AWS_BEARER_TOKEN_BEDROCK and the access key pair — the SDK picks up instance credentials automatically.
The install script downloads the latest release for your platform, checks it against the release's SHA256SUMS and puts bedrock-gateway in ~/.local/bin:
curl -fsSL https://raw.githubusercontent.com/sunerpy/bedrock-gateway-rust/main/scripts/install.sh | shirm https://raw.githubusercontent.com/sunerpy/bedrock-gateway-rust/main/scripts/install.ps1 | iexTOOL_VERSION=0.18.0 installs a given release (0.18.0 or later) and TOOL_INSTALL_DIR changes the directory. The archives are also on GitHub Releases, and cargo install bedrock-gateway-rust builds the binary from crates.io.
cargo build --release
API_KEY=sk-my-secret-key AWS_REGION=us-east-1 ./target/release/bedrock-gatewayAPI_KEY=testkey cargo runcurl http://localhost:8080/api/v1/health
# OK
curl http://localhost:8080/api/v1/chat/completions \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic.claude-3-5-sonnet-20241022-v2:0","messages":[{"role":"user","content":"Hello!"}]}'from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/api/v1",
api_key="sk-my-secret-key",
)
response = client.chat.completions.create(
model="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": "Explain serverless in one sentence."}],
)
print(response.choices[0].message.content)Streaming, tool calling, vision, and embeddings work the same way — no special configuration.
Four targets share the same binary and the same environment-variable interface.
| Target | Guide |
|---|---|
| Standalone binary | Run cargo build --release, ship target/release/bedrock-gateway + config/ |
| Docker (distroless) | Pull sunerpy/bedrock-gateway-rust — Deploy → Docker |
| ECS / Fargate + ALB | CloudFormation one-click — Deploy → ECS / Fargate |
| Lambda + Function URL | Lambda Web Adapter, no Lambda code — Deploy → Lambda |
aws cloudformation deploy \
--template-file deployment/BedrockGatewayFargate.template \
--stack-name bedrock-gateway \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
ApiKey=sk-my-secret-key \
VpcId=vpc-... \
SubnetIds=subnet-...,subnet-...# Build the Lambda container image
docker build -f deployment/lambda/Dockerfile -t bedrock-gateway:lambda .
# Push to ECR, then:
aws cloudformation deploy \
--template-file deployment/BedrockGatewayLambda.template \
--stack-name bedrock-gateway-lambda \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
ImageUri=<ECR_URI>:latest \
ApiKeySecretArn=arn:aws:secretsmanager:...Lambda has a 10-minute maximum timeout. For long-running streaming sessions, prefer ECS/Fargate.
All settings are environment variables. The gateway accepts both an APP_ prefix and bare names (e.g., API_KEY or APP_API_KEY); bare names take precedence.
| Variable | Purpose |
|---|---|
API_KEY |
Bearer token your clients send to the gateway. Pick any string. |
AWS_BEARER_TOKEN_BEDROCK |
Bedrock API Key the gateway presents to AWS (recommended). Or omit and use SigV4. |
AWS_REGION |
AWS region for Bedrock calls. Defaults to us-west-2. |
These two keys go in opposite directions — don't mix them up:
API_KEYis what your clients send to the gateway.AWS_BEARER_TOKEN_BEDROCKis what the gateway sends to AWS Bedrock.
The gateway auto-selects — no switch needed:
AWS_BEARER_TOKEN_BEDROCK(aliasBEDROCK_API_KEY) — Bedrock API Key, sent asAuthorization: Bearer. Bypasses SigV4 entirely. Recommended for new deployments.- SigV4 credential chain — resolves
AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, thenAWS_PROFILE, then IMDS / ECS task role.
Prefer fetching API_KEY from a secrets store. Priority order:
API_KEY_PARAM_NAME— SSM Parameter StoreAPI_KEY_SECRET_ARN— Secrets Manager (secret must contain anapi_keyfield)API_KEY— plaintext env var
Show all optional variables
Model defaults
| Variable | Default | Description |
|---|---|---|
DEFAULT_MODEL |
anthropic.claude-3-5-sonnet-20241022-v2:0 |
Model used when the client omits model |
DEFAULT_EMBEDDING_MODEL |
cohere.embed-multilingual-v3 |
Embedding model used when model is omitted |
Feature flags
| Variable | Default | Description |
|---|---|---|
ENABLE_CROSS_REGION_INFERENCE |
true |
Transparently route cross-region inference profiles |
ENABLE_APPLICATION_INFERENCE_PROFILES |
true |
Surface inference profile metadata in GET /models |
ENABLE_PROMPT_CACHING |
true |
Auto-inject cache points for Claude and Nova models |
CONFIG_DIR |
config |
External config directory path (overrides embedded defaults) |
Server
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP listen port |
BIND_ADDR |
0.0.0.0 |
Network interface to bind |
API_ROUTE_PREFIX |
/api/v1 |
Path prefix for all endpoints |
MAX_BODY_SIZE_MB |
20 |
Maximum accepted request body size in MB (raised from axum's 2 MB default so base64-encoded image payloads fit) |
Logging
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
Verbosity: trace / debug / info / warn / error |
DEBUG |
false |
Enable verbose error details in responses |
Timeouts and retries
| Variable | Default | Description |
|---|---|---|
AWS_CONNECT_TIMEOUT_SECS |
60 |
TCP connection timeout to AWS |
AWS_READ_TIMEOUT_SECS |
900 |
Response read timeout (15 min, accommodates long streams) |
RESPONSES_STREAM_IDLE_TIMEOUT_SECS |
180 |
Maximum silence between upstream Responses events before a terminal response.failed |
MODEL_CATALOG_REFRESH_SECS |
3600 |
Interval between background re-listings of the Bedrock model catalog, so new models appear in /models and pass the image check without a restart; 0 lists at boot only |
AWS_MAX_RETRY_ATTEMPTS |
8 |
Retries on transient throttling or 5xx failures |
GPT-5.x / bedrock-mantle
| Variable | Default | Description |
|---|---|---|
MANTLE_BASE_URL_TEMPLATE |
https://bedrock-mantle.{region}.api.aws/openai/v1 |
Upstream URL for GPT-5.x models (Responses surface). {region} is replaced with AWS_REGION at call time. |
MANTLE_CHAT_BASE_URL_TEMPLATE |
https://bedrock-mantle.{region}.api.aws/v1 |
Upstream URL for gpt-oss chat models (/chat/completions surface, no /openai prefix). {region} is replaced with AWS_REGION at call time. |
Config files live in config/ alongside the binary and are read at startup. They are also embedded in the binary as a fallback — the gateway always starts, even if the files are absent.
| File | Purpose |
|---|---|
config/models.toml |
Model capability registry: capability flags, reasoning paths, cache thresholds |
config/regions.toml |
Cross-region routing rules |
config/embeddings.toml |
Embedding model registry (Cohere / Titan / Nova families) |
config/app.toml |
Application defaults — lowest priority |
Adding a new model requires only a config/models.toml entry:
[[model]]
match = "your-provider.your-model-id"
capabilities = []
[model.params]
max_tokens = 8192
context_window = 200000
# reasoning_path = "budget_tokens" # uncomment if the model supports extended thinkingAll endpoints require Authorization: Bearer <API_KEY>.
Base URL: http://localhost:8080/api/v1
curl http://localhost:8080/api/v1/chat/completions \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic.claude-3-5-sonnet-20241022-v2:0","messages":[{"role":"user","content":"Count to 5 slowly."}],"stream":true}'curl http://localhost:8080/api/v1/embeddings \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"cohere.embed-multilingual-v3","input":["Hello world","Bedrock gateway"]}'response = client.chat.completions.create(
model="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": "Solve step by step: ..."}],
extra_body={"reasoning_effort": "high"}, # none|minimal|low|medium|high|xhigh|max
)
# Reasoning appears inline as <think>...</think> inside contentGPT-5.4, GPT-5.5, and GPT-5.6 Sol/Terra/Luna use the Mantle Responses upstream. They are also available on /api/v1/chat/completions through the built-in stateless Responses-to-Chat adapter. Reasoning summaries render as <think>, reasoning token usage is preserved, and function-tool continuations use authenticated rsc_v1 capsules. See the adapter design.
Non-streaming:
curl http://localhost:8080/api/v1/responses \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","input":"say hello","max_output_tokens":64,"stream":false}'Streaming (raw SSE, terminates on response.completed — no [DONE]):
curl http://localhost:8080/api/v1/responses \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4","input":"count to 5","stream":true}'Chat Completions with reasoning:
curl http://localhost:8080/api/v1/chat/completions \
-H "Authorization: Bearer sk-my-secret-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.6-sol","messages":[{"role":"user","content":"Solve 17 * 23"}],"reasoning_effort":"high","stream":true}'Add to ~/.codex/config.toml:
[model_providers.bgw]
name = "Bedrock Gateway"
base_url = "http://localhost:8080/api/v1"
wire_api = "responses"
requires_openai_auth = false
[model_providers.bgw.env_key]
name = "BGW_API_KEY"
description = "Bearer token for the Bedrock gateway"export BGW_API_KEY=sk-my-gateway-key
codex --provider bgw --model anthropic.claude-3-5-sonnet-20241022-v2:0src/
├── main.rs / lib.rs # Entry point, crate root
├── error.rs # AppError, OpenAI error envelope
├── telemetry.rs # tracing subscriber, dynamic log level
├── openai/ # Wire types: ChatRequest, ChatResponse, ResponsesRequest, ...
├── domain/ # Provider-agnostic traits: ChatProvider, ResponsesProvider, ...
├── config/ # Settings, ModelCapabilityConfig, RegionRoutingConfig, ...
├── bedrock/ # AWS Bedrock backend: translate, cache, reasoning, stream, ...
└── server/ # axum router, auth middleware, AppState
The codebase is strictly layered — dependencies flow downward only. The Bedrock backend implements the domain traits; adding a non-Bedrock backend means implementing those traits without touching any existing code.
For the full architecture reference, contract rules, and contributor guidelines, see the Architecture & Contracts guide.
For caching behavior, reasoning budget paths, and cross-region inference profile details, see the Caching & Reasoning deep dive.
The authoritative list is config/models.toml and the live GET /api/v1/models endpoint. The registry currently covers:
- Claude — Sonnet 4.x / 5 / 5.5, Haiku 4.5 / 5.5, Opus 4.x / 5 / 5.5, Fable 5 / 5.1 (via cross-region inference profiles such as
global.anthropic.claude-sonnet-5-5).response_format/ Responsestext.formatis sent to Bedrock as structured output for every model; Bedrock answers 400 for a model without it (for example Fable 5) - Amazon Nova — multimodal and text models
- DeepSeek — v3 (string-form reasoning path)
- xAI Grok 4, Moonshot Kimi K3 —
temperature/top_pare dropped (the models reject them) - GPT-5.x —
gpt-5.4,gpt-5.5, andgpt-5.6-sol/terra/lunaon/api/v1/responsesplus adapted/api/v1/chat/completions, via the AWS Bedrock mantle Responses upstream. Region availability is declared inconfig/models.toml. RequiresAWS_BEARER_TOKEN_BEDROCK; reasoning + tool continuation additionally requires the sharedCHAT_REASONING_CAPSULE_*keyring. - gpt-oss —
gpt-oss-120bandgpt-oss-20bon/api/v1/chat/completionsonly, via the AWS Bedrock mantle upstream (chat_backend = "mantle"). Byte-level raw SSE passthrough; the gateway appendsdata: [DONE]at the stream tail. Not available on/completions. Listed inGET /modelsunder the bare alias namesgpt-oss-120b/gpt-oss-20b. Region-gated:us-east-1/us-east-2/us-west-2. RequiresAWS_BEARER_TOKEN_BEDROCKto be set (otherwise those models are disabled with a WARN; the gateway still starts). - GPT-6.x —
gpt-6.1-sol,gpt-6-sol,gpt-6-luna, andgpt-6-astraon both/api/v1/chat/completionsand/api/v1/responsesthrough Bedrock Converse. The bare names alias to theglobal.openai.*cross-region inference profiles; theus./global.profile ids also work directly.reasoning_effortmaps toreasoning.effort,temperature/top_pare dropped (the models reject them), andresponse_format(chat) ortext.format(Responses) uses the OpenAItext.formatshape (strict schemas honored) - Any Bedrock foundation model or inference profile accessible in your account — the catalog is listed from the control plane at startup and re-listed every
MODEL_CATALOG_REFRESH_SECS
Adding a model requires only a config/models.toml entry and no recompile.
cargo build --release # release binary → target/release/bedrock-gateway
cargo test --all-features # unit + golden replay tests (no AWS credentials needed)
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --allGate before every push (make hooks runs it on git push):
cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test --all-featuresGolden fixtures in tests/golden/ are pinned offline and run in CI without AWS credentials. Live integration tests require real credentials:
BEDROCK_INTEGRATION=1 AWS_PROFILE=<your-profile> cargo test -- --ignoredSee the Contributing Guide for contributor guidelines.
| Resource | Link |
|---|---|
| 中文文档 / Chinese README | Read in Chinese |
| Contributing | Contributing Guide |
| Code of Conduct | Code of Conduct |
| Caching and reasoning | Caching & Reasoning deep dive |
| Docker deployment | Deploy → Docker |
| ECS/Fargate deployment | Deploy → ECS / Fargate |
| Lambda deployment | Deploy → Lambda |
| Architecture and contracts | Architecture & Contracts guide |
This project is inspired by the Python gateway concept in aws-samples/bedrock-access-gateway. The Rust implementation is a full rewrite with wire-exact OpenAI compatibility, automatic prompt caching, Responses API support, and production-grade observability.