Magic Tools
Hands-OnBy CooconSeptember 20, 20268 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 hellogoodbye

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.

Related Articles

DeepSeek Says 1M, Claude Code Says 200K: I Measured Both and Neither Number Is the Real Limit

DeepSeek advertises a 1M context window. Point Claude Code at it and Claude Code reports contextWindow 200000 for the same model. I measured what actually happens. DeepSeek's real ceiling is 1,048,576 tokens — literally 2^20, not one million — and it covers input plus your max_tokens budget, proven with a controlled pair. A needle planted at position zero was retrieved correctly at 1,039,744 tokens. Claude Code refuses client-side long before that, in 25ms with zero API calls, and its gate is not on tokens at all: it fires at roughly 480,000 characters. Feed it high-entropy text and 478,000 characters sails through carrying 309,567 real tokens — 55% past the 200K window it just claimed. And in ordinary use you reach none of these, because Bash output over exactly 30,000 characters never enters context at all.

claude-codelong-context+5
hands-onSep 20, 20267 min
4

Service Up, Ports Open, Certs Valid, VPN Dead for 4 Hours: Tailscale Took Over DNS and Left the Proxy Box With No Upstream

A Los Angeles VPS running sing-box (VLESS-REALITY + Hysteria2) lost its VPN the day after Tailscale was installed. systemctl, ports and certificates were all fine. The root cause was in /etc/resolv.conf: Tailscale manages DNS by default, the tailnet had no global nameservers, and when dhclient renewed its lease tailscaled read an empty resolv.conf and dropped its upstream list. From then on every public domain got SERVFAIL, and the REALITY handshake could not even resolve www.apple.com. Full timeline, the evidence for each step, three fixes, and the rules we added to CLAUDE.md so an AI assistant (Claude Code) does not walk into this again.

claude-codetroubleshooting+8
pitfallsSep 17, 20266 min
69

Reproducing an Injection Chain That Cracks Claude Code Auto Mode: the Model Refuses the Malicious Binary, Then Writes Code That Pwns Itself

In late August embracethered published an attack chain where a plain 'summarize this page' request drags auto-mode Claude Code to a 60–80% code-execution rate — while Anthropic's commissioned third-party test reported 0.00%. I took the chain apart and tested it stage by stage in an isolated environment: the endpoint that nudges the model from WebFetch to curl, and the crux — the model's own 'safe' decision to refuse the unknown binary and write its own Python decoder instead lands straight on a same-name struct.py planted in the extracted directory. The deterministic parts (branching + module-shadow poison + mitigation controls) reproduce fully on my machine with real evidence; the live end couldn't complete a full RCE here because the classifier rate-limited and failed closed — flagged honestly. Ends with mitigations that actually help.

claude-codeauto-mode+5
hands-onAug 31, 20269 min
169

Cracking Open Claude Code's Auto-Mode Classifier: A 116K-Char System Prompt, Dissected Line by Line

My earlier retest confirmed auto mode calls the session model as a classifier before each risky Bash — but what it receives stayed a black box. This time I captured the full request: a 116,879-char system prompt opening 'You are a security monitor for autonomous AI coding agents.' I quote it verbatim to dissect the threat model, two-tier rules (1 HARD BLOCK / 68 SOFT BLOCK / 17 ALLOW), and two-stage evaluation — stage 1 grades harm only, stage 2 layers intent on top. Every number read out this session.

claude-codepermissions+5
hands-onAug 30, 202612 min
254