Claude Code on Bedrock with Open Weight Models
How to configure nearly an order of magnitude cheaper usage of Claude Code on Bedrock through usage of open weight models.
Running open-weight models as your Claude Code driver
How to point Claude Code at non-anthropic models on Bedrock through a translation proxy, and the traps that sit between "the model is in my account" and "it actually works."
The idea
Claude Code talks to one shape of API: the Anthropic Messages format. But your Bedrock account can hold models that don't speak that format at all, open-weight ones like Moonshot's Kimi, Meta's Llama, or any other foundation model AWS exposes. Those speak OpenAI-style or Bedrock's Converse API instead.
A small translation proxy sits between the two and lets Claude Code drive any of them. You pick the proxy (this post uses LiteLLM , but the pattern is the same for any Anthropic-compatible gateway) and you pick the model. The proxy presents an Anthropic
/v1/messages endpoint to Claude Code on one side, and translates to whatever the upstream model speaks on the other.1
2
Claude Code ──Anthropic /v1/messages──▶ translation proxy ──▶ any Bedrock model
(ANTHROPIC_BASE_URL) (LiteLLM, etc.) (Kimi, Llama, ...)The shape is simple. The traps are not, and a few of them are specific to running non-Claude models this way. The rest of this post walks a concrete example end to end (Claude Code driving Kimi K2.5), but the prerequisites, the proxy mechanics, and every trap apply to any model/proxy combination.
Total setup, once you know the traps: ~10 minutes. Finding the traps: a handful of frontier model tokens.
Prerequisites
This post assumes you already have Claude Code talking to Bedrock the normal way, authenticating to AWS,
CLAUDE_CODE_USE_BEDROCK=1 set, and Claude models working in /model. If you're not there yet, set that up first; it's the foundation everything below builds on.- Anthropic's official guide: Claude Code on Amazon Bedrock
- A fantastic Reference Architecture for Claude Code on Bedrock written by a fellow AWS Solution Architect
You'll also need: AWS credentials/SSO that can call Bedrock, model access granted for the model you want (request it in the Bedrock console if needed), and Python with a way to install packages (
uv or pip).Counterintuitively, the existingCLAUDE_CODE_USE_BEDROCKsetup is also the source of the first two traps below. Getting it working is step one; getting Claude Code to stop using it for these sessions is step two.
Where this started
I opened
/model in Claude Code expecting to see Kimi. It wasn't there. I knew Kimi was on Bedrock, and a quick check proved it was sitting right in my account:1
2
3
$ aws bedrock list-foundation-models \
--query "modelSummaries[?contains(modelId,'kimi')].modelId" --output json
["moonshotai.kimi-k2.5", "moonshot.kimi-k2-thinking"]So the model is there. Why won't Claude Code show it? That question turned out to have a four-layer answer, and peeling each layer revealed the next.
The core problem: two languages
Claude Code's main loop speaks Anthropic Messages,
POST /v1/messages, responses shaped like content:[{type:"text"}] with a stop_reason, and server-sent events like content_block_delta for streaming.Kimi on Bedrock speaks OpenAI / Bedrock Converse, responses shaped like
choices:[{message:{...}}] with a finish_reason.I proved the mismatch by hand. A raw
invoke-model against Kimi with an Anthropic-style body came back as:1
{"choices":[{"message":{"content":"KIMI_OK"}}],"usage":{"completion_tokens":5}}That's OpenAI format. Claude Code's client tries to read
content[] and stop_reason, finds choices[] instead, and gives up. They cannot talk directly. Something has to translate, and that something is LiteLLM , which exposes a native Anthropic /v1/messages endpoint and bridges it to Bedrock both ways.1
2
Claude Code ──Anthropic /v1/messages──▶ LiteLLM (:4000) ──Bedrock Converse──▶ moonshotai.kimi-k2.5
(ANTHROPIC_BASE_URL) (the translator) (your AWS creds, no API key)The shape is simple. The traps are not.
Trap 1, /model is allowed to hide your model
The first instinct is "the picker just needs the right config." It doesn't, the picker physically cannot show Kimi while you're in Bedrock mode.
Per Claude Code's gateway protocol, model discovery (the thing that populates
/model from a gateway) is skipped whenever any CLAUDE_CODE_USE_* provider variable is set. If you launched Claude Code with CLAUDE_CODE_USE_BEDROCK=1, which most Bedrock users have in their shell profile, it never even asks the proxy what models exist.And there's a second filter: discovery only lists model IDs that start with
claude or anthropic. Name your model kimi-k2 and it's invisible in the picker forever; name it claude-kimi-k2 and it shows up as a "From gateway" row.Lesson: to get an open-weight model into/model, you must (a) launch without the Bedrock/Mantle provider flags so discovery runs, and (b) prefix the model name withclaude-.
Trap 2, the header that says you've already lost
I built a launcher that stripped
CLAUDE_CODE_USE_BEDROCK and pointed at the proxy. Relaunched. The header read:1
claude-kimi-k2 · Amazon Bedrock (Mantle)hi → "There's an issue with the selected model (claude-kimi-k2)."That word (Mantle) is the tell. Claude Code was still in a Bedrock provider mode, ignoring my proxy entirely, treating
claude-kimi-k2 as a Bedrock Mantle model ID, which of course doesn't exist.The cause was hiding in
~/.zshrc. I had been stripping one flag. There were two:1
2
export CLAUDE_CODE_USE_BEDROCK=1
export CLAUDE_CODE_USE_MANTLE=1 # <-- this one survived and kept me in Bedrock modeLesson: the proxy is only used when everyCLAUDE_CODE_USE_*flag is gone. When the header shows your gateway address instead of "Amazon Bedrock," you've escaped.
Trap 3, the official tutorial is wrong for open-weight models
Header fixed. Now the proxy was actually in the loop. New failure: the model "responded" with nothing. Empty bubble.
The raw stream explained why:
1
2
3
4
event: content_block_start
event: content_block_stop ← stop arrives BEFORE the content
event: content_block_delta
data: {... "text": ""} ← and the content is emptyInvalid event order, zero content, zero tokens. This is the part that cost me the most, because I'd followed the official LiteLLM Claude Code tutorial to the letter, and it explicitly recommends the
bedrock/invoke/ model prefix as "the smoother path for Claude Code traffic."That advice is correct, for Claude models on Bedrock. For open-weight Kimi,
bedrock/invoke/ has a broken streaming translation: it works non-streaming, but Claude Code always streams, so you get empty output every time. The fix was a one-word change in the config:1
2
# model: bedrock/invoke/moonshotai.kimi-k2.5 # broken streaming for Kimi
model: bedrock/converse/moonshotai.kimi-k2.5 # correctWith
converse, the stream came back valid, start → delta(" Hi.") → delta → stop with real token counts.Lesson: tutorials targeting Claude-on-Bedrock don't transfer 1:1 to open-weight Bedrock models. For Kimi, usebedrock/converse/, notbedrock/invoke/. This isn't documented anywhere I could find, it's the single most valuable thing in this post.
Trap 4, the parameter Bedrock won't accept
So close. The header now read
claude-kimi-k2 · API Usage Billing, a real request was reaching Kimi. And it errored, cleanly:1
2
API Error: 400 bedrock does not support parameters: ['reasoning_effort'],
for model=converse/moonshotai.kimi-k2.5. To drop these, set litellm.drop_params=TrueClaude Code sends
reasoning_effort (that /effort high setting in the UI). Bedrock Converse rejects it for Kimi. The error message hands you the fix:1
2
litellm_settings:
drop_params: true # strip params the upstream rejects instead of erroringRestart,
hi → Kimi replied. Done.Lesson: Claude Code sends a richer parameter set than open-weight Bedrock models accept.drop_params: truelets LiteLLM quietly drop the unsupported ones rather than failing the call.
The working setup
config.yaml1
2
3
4
5
6
7
8
9
10
11
12
13
model_list:
- model_name: claude-kimi-k2 # "claude-" prefix → visible in /model
litellm_params:
model: bedrock/converse/moonshotai.kimi-k2.5
aws_region_name: us-east-1
- model_name: claude-kimi-thinking
litellm_params:
model: bedrock/converse/moonshot.kimi-k2-thinking
aws_region_name: us-east-1
litellm_settings:
master_key: os.environ/LITELLM_MASTER_KEY
drop_params: trueInstall & run (you need the
[proxy] extra, plain litellm won't start a server):1
2
uv pip install --python "$(which python3)" "litellm[proxy]"
LITELLM_MASTER_KEY=sk-local litellm --config ~/litellm/config.yaml --port 4000claude-kimi.sh, the launcher that dodges traps 1 & 2:1
2
3
4
5
6
7
8
9
10
exec env -u CLAUDE_CODE_USE_BEDROCK \
-u CLAUDE_CODE_USE_MANTLE \
-u ANTHROPIC_MODEL \
-u ANTHROPIC_DEFAULT_OPUS_MODEL -u ANTHROPIC_DEFAULT_SONNET_MODEL -u ANTHROPIC_DEFAULT_HAIKU_MODEL \
ANTHROPIC_BASE_URL="http://0.0.0.0:4000" \
ANTHROPIC_AUTH_TOKEN="sk-local" \
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 \
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \
claude --model claude-kimi-k2 "$@"A healthy launch shows
claude-kimi-k2 · API Usage Billing in the header.The debugging move that mattered most
When the UI just says "there's an issue," you're blind. The fix was to reproduce Claude Code's exact request with curl, streaming, system prompt,
reasoning_effort, and read the raw bytes:1
2
3
4
5
6
7
curl -s -N -X POST http://0.0.0.0:4000/v1/messages \
-H "Authorization: Bearer sk-local" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-kimi-k2","max_tokens":40,"stream":true,
"reasoning_effort":"high","system":"You are Claude Code.",
"messages":[{"role":"user","content":"say hi briefly"}]}'Every trap above was visible in that output, the empty stream, the bad event order, the 400, long before Claude Code's UI gave a useful hint.
One sub-trap: always sendContent-Type: application/json. Without it, curl sends form-encoded data and LiteLLM reports a bafflingInvalid model name passed in model=None. That's a curl artifact, not a real bug, Claude Code always sets the header.
Great it works, but how well?
I verified the plumbing: text, streaming, and tool-call format all round-trip correctly. I did not verify that Kimi K2.5 is actually good at driving Claude Code's full agentic loop, long multi-tool sequences, the giant system prompt, staying coherent across a real task. That's a model-capability question, and only real use answers it. If Kimi flails on a complex task, that's the model, not the proxy.
Also off the table while this is wired up: prompt caching (disabled by
DISABLE_EXPERIMENTAL_BETAS), and long-term stability (Claude Code adds beta capabilities every release; re-run the curl test after upgrades).Why bother?
Three reasons this is worth knowing even if you stay on Claude day-to-day:
- Cost/latency experiments, A/B a cheaper open-weight model on real tasks without leaving your tooling.
- The pattern generalizes, the same proxy fronts any LiteLLM-supported model (Llama, Mistral, local vLLM). Kimi is just the example.
- It demystifies the boundary, once you've watched the schemas translate byte-for-byte, "Claude Code only works with Claude" stops being a mystery and becomes a config choice.
The model was in my account the whole time. The distance between "available" and "usable" was four traps and one wrong word in a tutorial. Now you have the map.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article