Magic Tools
Hands-OnBy CooconSeptember 20, 2026337 views8 min read

Running Claude Code on DeepSeek: Everything Works, But the Cost Readout Lies by 38x

DeepSeek's API speaks the Anthropic format at https://api.deepseek.com/anthropic. Three environment variables and Claude Code talks to DeepSeek instead of Anthropic.

Plenty of posts tell you those three variables. Almost none tell you what degrades. That is the only question worth asking — an agentic coding tool is not a chat box, it lives or dies on tool calls, multi-turn state, and whether the numbers on your screen mean anything.

So I ran it. Claude Code 2.1.270, macOS, in an isolated config directory that never touches my real setup. The Claude Code runs below all target deepseek-v4-pro; deepseek-flash appears in the raw-endpoint mapping tests and as the Haiku slot.

Short version: the functionality is fine, the cost readout is not. Claude Code told me I had spent $1.71. DeepSeek charged me ¥0.32.

Background: why anyone wants this

Two reasons, and they pull in different directions.

The boring one is price. DeepSeek V4 Pro lists at $1.32 / 1M input tokens at peak and $0.66 off-peak, against Claude's per-million rates in a different bracket entirely. For a tool that re-sends a large system prompt on every single turn, that ratio compounds fast.

The interesting one is that Claude Code is, at this point, the most capable agentic harness in wide use — and it is the part you cannot easily rebuild. Being able to keep the harness and swap the engine underneath is genuinely useful, if the swap is clean.

Whether it is clean is an empirical question, so here is the measurement.

What I tested, and what I deliberately did not

Tested: connectivity, the model-name mapping, the local tool matrix, subagent spawning, prompt caching across turns, and real billed cost against the actual account balance.

Not tested (and why):

  • MCP servers. DeepSeek's compatibility table lists the API's mcp_servers field as Ignored, but that field is for Anthropic's server-side MCP. Claude Code's MCP is local stdio and never touches it. Testing the API field would prove nothing about the tool you actually use, and properly testing local MCP needs its own article.
  • Long-context behaviour at 1M. DeepSeek advertises a 1M context window. Claude Code reports contextWindow: 200000 for the model. Resolving which one wins needs a dedicated filling experiment; I am not going to guess from a metadata field.
  • Vision. The pricing page states v4-pro does not support vision while flash does. I did not exercise image input.

Everything below was run on one machine on 2026-09-20, with raw logs kept.

Setup

Three variables do the work:

export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic"
export ANTHROPIC_AUTH_TOKEN="$DEEPSEEK_API_KEY"   # not ANTHROPIC_API_KEY
unset ANTHROPIC_API_KEY
export ANTHROPIC_MODEL="deepseek-v4-pro"

Use ANTHROPIC_AUTH_TOKEN, not ANTHROPIC_API_KEY — with both set Claude Code prompts about the conflict. I also ran everything under a throwaway CLAUDE_CONFIG_DIR and a scratch working directory, so nothing here can reach a real project:

export CLAUDE_CONFIG_DIR="$PWD/cc-config"

Ground truth first: what the endpoint actually does

Before touching Claude Code I hit the raw endpoint, because if the mapping layer surprises you, everything above it is guesswork.

Model mapping is prefix-based — and the docs are wrong about the fallback

Model name sent Model returned Latency
claude-opus-5 deepseek-v4-pro 3.20s
claude-sonnet-5 deepseek-flash 1.03s
claude-haiku-4-5-20251001 deepseek-flash 1.32s
deepseek-v4-pro deepseek-v4-pro 1.97s
deepseek-flash deepseek-flash 1.44s
totally-made-up-model HTTP 400 0.38s

The claude-opus* → v4-pro and claude-sonnet*/claude-haiku* → flash mapping matches the documentation.

The last row does not. The docs say:

When you pass an unsupported model name to DeepSeek's Anthropic API, the API backend will automatically map it to the deepseek-flash model.

It does not. It returns a 400:

{"error":{"message":"The supported API model names are deepseek-flash, deepseek-v4-pro, but you passed totally-made-up-model.","type":"invalid_request_error"}}

So the real rule is: claude-* prefixes map, everything else must be an exact DeepSeek model name or you get a 400. If you are scripting model selection, do not rely on the documented fallback.

Raw endpoint model mapping: claude-* prefixes map, unknown names return HTTP 400

v4-pro returns thinking blocks by default

Ask for max_tokens: 64 and the reply looks empty. It isn't — the budget went to reasoning:

"content": [
  { "type": "thinking", "thinking": "We need answer user asks \"Say OK.\" ...", "signature": "4b525c96-..." },
  { "type": "text", "text": "OK" }
]

Thinking mode is on by default for both models per DeepSeek's pricing page. If you are writing a thin client against this endpoint, budget max_tokens for reasoning or you will ship a bug where short answers come back blank. (Claude Code handles this correctly; this bites custom integrations.)

/v1/models 404s, harmlessly

GET /v1/models?limit=1000  -> HTTP 404
GET /v1/models             -> HTTP 404

Claude Code probes this on startup and carries on regardless. Cosmetic.

The tool matrix: everything works

This is the part that could have gone badly and didn't. Each row is one claude -p run in the scratch directory, and I verified the side effects on disk, not just the model's claim of success.

Test Turns Input Cache read Output Wall Result Side effect verified
Read a file 2 34,186 34,176 107 8.7s hello from lab —
Write a file 2 34,265 34,176 131 9.0s DONE written.txt = WRITTEN_OK ✅
Run Bash 2 34,171 34,176 93 6.8s BASH_OK —
Glob 2 34,284 34,432 454 10.3s both filenames —
Edit in place 3 34,474 68,480 272 8.7s EDITED probe.txt hello→goodbye ✅

Subagents work too. Asking for the Agent tool produced a real spawn, and tracing the stream shows the actual call chain:

t9b  tools actually invoked: ['Agent', 'Bash']

Tool matrix all green, with Write/Edit side effects verified by reading the files back

Prompt caching works. The first (cold) call shows cache_read_input_tokens: 0; every subsequent call reads ~34K from cache. I initially misread that cold-start zero as "caching is broken" — it isn't, and if you only look at your first request you will reach the same wrong conclusion.

Note the constant in that table: ~34,000 input tokens on every turn, before you type anything. That is Claude Code's system prompt plus tool definitions. It is why the cache-hit price matters more than the headline input price.

The headline: the cost readout is off by 38x

Claude Code prints a cost. Against DeepSeek, that number is fiction.

I ran ten identical turns and measured the actual account balance before and after, rather than deriving anything from a price list:

Value
Claude Code reported (sum of 10 runs) $1.7097
DeepSeek balance before ¥37.58
DeepSeek balance after ¥37.26
Real spend ¥0.32 ≈ $0.045
Over-report 38x

Where does the fake number come from? One cold run reported $0.171215 on 34,098 input + 29 output tokens. Solving for the rate:

34,098 × $5/1M  +  29 × $25/1M  =  $0.171215   ← exact match

Claude Code is billing DeepSeek tokens at $5 / $25 per million — Claude Sonnet's rate card. It even flags its own uncertainty in the JSON ("costBasis": "unknown") and prints a confident dollar figure anyway.

Two practical consequences:

  1. /cost and the JSON total_cost_usd are unusable here. Anything you build on them — budget alarms, per-task accounting, "is this worth it" decisions — will be wrong by more than an order of magnitude.
  2. Use DeepSeek's own GET /user/balance if you need real numbers. Caveat: it reports to two decimal places in CNY, so single calls are below its resolution — batch until the delta is measurable. My conversion uses ¥7.1/$; the 38x ratio is insensitive to reasonable rate choices.

Ten runs: Claude Code reported $1.7097, DeepSeek actually billed ¥0.32

One honest limit: balance-delta at ¥0.01 precision over ten runs is a coarse instrument. The direction and order of magnitude are solid; treat "38x" as ~40x, not as four significant figures.

The failure that is not DeepSeek's fault

WebFetch failed on every attempt:

is_error=True  Unable to verify if domain example.com is safe to fetch.
This may be due to network restrictions or enterprise security policies blocking claude.ai.

The model then quietly fell back to Bash + curl and got the right answer anyway — which is exactly the kind of silent degradation worth catching.

My first hypothesis was that CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 — which DeepSeek's own integration guide recommends — was blocking the domain-safety check. That hypothesis was wrong. Controlled test:

Group DISABLE_NONESSENTIAL_TRAFFIC Proxy WebFetch
A 1 none ❌ fails
B unset none ❌ fails
C 1 on ✅ The page title is "Example Domain"

The variable is irrelevant. The domain-safety check calls back to Anthropic's infrastructure, and claude.ai is unreachable from mainland China without a proxy. I hit the identical error earlier the same day from a session running on genuine Anthropic credentials.

So: if you are in China and WebFetch breaks after switching to DeepSeek, do not blame DeepSeek. It was broken before you switched. Set a proxy.

Three-group control: unrelated to DISABLE_NONESSENTIAL_TRAFFIC, works with a proxy

I am spelling this out because it is the single easiest false finding to publish here, and I nearly did.

Cosmetic noise you can ignore

Every call writes this to stderr:

[claude-code:unrecognized_model] {"model":"deepseek-v4-pro","query_source":"sdk"}

Claude Code does not know the model name. It proceeds correctly. If you are parsing stderr in a script, filter it.

Verdict

Use it for: bulk mechanical work where tool calls dominate and per-token cost matters — refactors, test scaffolding, log spelunking, anything you would feel bad about spending Opus tokens on. The tool layer is genuinely solid; nothing in my matrix silently produced a wrong side effect.

Do not use it for: anything where you need the cost display to be true, or where you are relying on Claude Code's reported context window matching the model's real one. Both are wrong right now.

The one thing to change immediately if you set this up: stop reading /cost. Put DeepSeek's balance endpoint in a two-line script instead.

Pitfalls, condensed

  1. The docs' unknown-model fallback does not exist — non-claude-* names that aren't exact DeepSeek model IDs return HTTP 400, not a silent remap to flash.
  2. Thinking blocks eat max_tokens — a small budget yields an apparently empty reply. Affects custom clients, not Claude Code.
  3. Cold-start cache reads are zero — do not conclude caching is broken from your first request.
  4. total_cost_usd is Sonnet pricing applied to DeepSeek tokens — ~38x high, measured against the real invoice.
  5. WebFetch failures in China are a proxy problem, not a DeepSeek problem — verify with a control group before writing it up as a compatibility gap.
  6. /v1/models 404 and unrecognized_model on stderr are both harmless — do not chase them.

Tested 2026-09-20 on Claude Code 2.1.270, macOS/Apple Silicon, against deepseek-v4-pro (DeepSeek-V4-Pro-0813) and deepseek-flash (DeepSeek-V4.1-Flash). Pricing figures read from DeepSeek's official pricing page on the same day; spend measured via GET /user/balance before and after.

Get field notes like this every Saturday

Subscribe to Dev Breakfast: daily AI coding picks at 8:00, plus a Saturday roundup of this week's hands-on tests with Claude Code / Codex / local models. Written in Chinese.

Related Articles

Blocking rm -rf in Claude Code: A Deny Rule Stopped 14 Deletes in Two Weeks, 7 Other Spellings Still Get Through, and a Hook That Closes the Gap

To keep Claude Code from deleting things by accident, I added a deny rule, Bash(rm -rf *), to my global settings. Across my sessions from October 7 to 21 it blocked 14 deletes. After each denial the model picked a new directory name 8 times and dropped the delete 6 times. It never tried another spelling. Then I replayed 22 delete commands verbatim against Claude Code 2.1.285 through a stub. rm -fr, rm -r -f, rm -Rf and rm -r deleted the directory in manual, auto and bypass mode alike. /bin/rm, find -delete and bash -c deleted it in manual and bypass mode. In auto mode, rm -fr inside the working directory skips the classifier because it 'would be allowed in acceptEdits mode'. A second, built-in dangerous-rm check fires on targets like $D/*, even in bypass mode, but loses track of a variable assignment when a heredoc comes earlier in the same command. The post ends with a PreToolUse hook that blocks every recursive spelling in all three modes and tells the model what to do instead.

claude-codehooks+5
pitfallsOct 21, 20268 min
482

Claude Code Auto Mode with DeepSeek, GLM or Kimi: What Each "temporarily unavailable" Reason Means and When Retrying Is Pointless (Fault-Injection Test)

On a third-party model, Claude Code auto mode sends one extra classifier request per side-effecting action, from your session model to the same backend: a 132,470-character system prompt, about 29.7K input tokens per check. When it fails, you see "xxx is temporarily unavailable, so auto mode cannot determine the safety of Bash". We injected faults and recorded each message: 500 (server error), 429 (rate-limited), 529 (overloaded) and connection resets (connection failed) are retried 5 times; no reply in 60 s gives (timed out). **No parentheses means a 4xx rejection: tried once, and retrying never helps.** A reply with no verdict gives "could not evaluate"; three in a row bring back manual approval. CLAUDE_CODE_AUTO_MODE_MODEL has no effect. Claude Code 2.1.285, real DeepSeek v4-pro and MiMo v2.5 backends.

claude-codedeepseek+7
pitfallsOct 11, 202612 min
540

Claude Code "Prompt is too long" vs "maximum context length": Why Auto-Compact Works for One and Not the Other (Tested)

Claude Code decides a request was too long by matching the error text. If the backend says prompt is too long or input is too long for requested model, it auto-compacts the conversation and retries — invisible to you. DeepSeek and OpenAI-style gateways say This model's maximum context length is …, which it doesn't recognize: you get API Error: 400 on every turn. Manual /compact works; the better fix is to declare the real window with CLAUDE_CODE_MAX_CONTEXT_TOKENS (non-claude- model IDs) or CLAUDE_CODE_AUTO_COMPACT_WINDOW (claude- IDs) so it compacts before hitting the limit. Tested on Claude Code 2.1.285 against a local stub.

claude-codedeepseek+6
pitfallsOct 5, 20267 min
568

Claude Code Stuck on a Spinner With No Response: It Waits 6 Minutes per Attempt, and 10 Retries Can Hang It for Over an Hour (Tested)

When Claude Code spins without output or sits at Retrying in 0s, the backend is usually not sending anything. Tested on 2.1.285 (API key + ANTHROPIC_BASE_URL): with no response headers it waits 6 minutes (360 s) per attempt before timing out — setting API_TIMEOUT_MS to 600000 or 900000 doesn't change that; only lower values work. With the default 10 retries it can hang for over an hour. A gateway that buffers the reply until generation finishes will never deliver a reply that takes over 6 minutes. Press Esc to interrupt. All tested against a local stub.

claude-codetroubleshooting+5
pitfallsOct 5, 20269 min
478