AWS Builder Center

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.

Hacker by Nature // Leader by Trade

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.
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 existing CLAUDE_CODE_USE_BEDROCK setup 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 with claude-.

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 mode
Lesson: the proxy is only used when every CLAUDE_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 empty
Invalid 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 # correct
With 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, use bedrock/converse/, not bedrock/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=True
Claude 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 erroring
Restart, hi → Kimi replied. Done.
Lesson: Claude Code sends a richer parameter set than open-weight Bedrock models accept. drop_params: true lets LiteLLM quietly drop the unsupported ones rather than failing the call.

The working setup

config.yaml
1
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: true
Install & 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 4000
claude-kimi.sh, the launcher that dodges traps 1 & 2:
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env bash
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 send Content-Type: application/json. Without it, curl sends form-encoded data and LiteLLM reports a baffling Invalid 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article