Stephan Miller
Claude Code on DeepSeek: One Command

Claude Code on DeepSeek: One Command

I’ve probably mentioned that I’ve been trying to stop paying Opus prices for work that does not deserve it. I wrote a whole cheapskate’s guide about it, and since then I split my rig so the expensive model only plans and Opencode with cheaper models does the grind. That worked. But it gave me two different tools and two different sets of muscle memory, and some nights I just want the Claude Code I already know.

So when I saw a post going around with three export lines that are supposed to point Claude Code at DeepSeek, I gave it a try.

It did not work. It gave me an error message. And the fix was a single path segment.

Here is the working version, why the copy-paste version fails, and a launcher that switches to DeepSeek models in one command.

What DeepSeek Gives You

Claude Code talks the Anthropic Messages API. It does not speak OpenAI-style chat completions. So pointing it at some random provider’s endpoint won’t work.

What makes this trick possible is that DeepSeek runs an Anthropic-compatible endpoint. Ask the API what it will serve you instead of trusting a model name you read somewhere:

curl -s https://api.deepseek.com/models \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY"

Two models came back for me:

{"object":"list","data":[
  {"id":"deepseek-v4-flash","object":"model","owned_by":"deepseek"},
  {"id":"deepseek-v4-pro","object":"model","owned_by":"deepseek"}
]}

That is all you get. deepseek-v4-flash is the fast cheap one, deepseek-v4-pro is the heavier one. Two models is a really short list when you are used to looking at OpenRouter’s wall of options.

The Issue That Cost Me Time

The instructions I copied said to set this:

export ANTHROPIC_BASE_URL="https://api.deepseek.com/v1"

Looks right. Every API you have ever used has a /v1 on it. And Claude Code starts up fine with it.

Then you send a message and get this:

There's an issue with the selected model (deepseek-v4-flash).
It may not exist or you may not have access to it.
Run --model to pick a different model.

So I checked the model. Is it a typo? Does my key have access? But the model was fine the whole time.

Claude Code appends /v1/messages to whatever you put in ANTHROPIC_BASE_URL. Give it a base ending in /v1 and it builds this:

https://api.deepseek.com/v1/v1/messages

The Issue That Cost Me Time

That 404s, Claude Code catches the failure, and it reports that your model did not work out. Wrong diagnosis, right symptom.

This is documented behavior. The model configuration docs say that behind a custom ANTHROPIC_BASE_URL, your provider defines the model names, so Claude Code passes any string through without validating it. It cannot tell a bad model name from a bad URL.

The correct base is the Anthropic root, no version segment:

export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic"

I tested it with raw curl before touching the launcher:

curl -s https://api.deepseek.com/anthropic/v1/messages \
  -H "x-api-key: $DEEPSEEK_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"deepseek-v4-flash","max_tokens":16,
       "messages":[{"role":"user","content":"say OK"}]}'

Real Anthropic-shaped response, thinking block and all.

Why I Did Not Just Export the Variables

The easy way seems to be adding those exports to your shell config and be done. I did not want that.

The moment ANTHROPIC_API_KEY is set in your environment, it takes over your claude.ai login. Claude Code even tells you so on startup:

⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another
  auth source is set and takes precedence over your claude.ai login

So if you export those globally, you have not added a DeepSeek option. You have replaced Claude Code. Every session, every project, forever, until you remember why and unset it. That is a great way to be confused at two in the morning next month.

Instead I wanted a new command for running it this way, so when I run claude I have no issues.

The Fish Function

I use fish, and this is one of those cases where it pays for itself. Fish autoloads any function file in ~/.config/fish/functions/, so saving claude-ds.fish there gives me a new command. My DeepSeek key lives in a universal variable (set -Ux DEEPSEEK_API_KEY sk-...), which persists across sessions.

function claude-ds --description "Launch Claude Code against the DeepSeek API"
    if not set -q DEEPSEEK_API_KEY; or test -z "$DEEPSEEK_API_KEY"
        echo "claude-ds: DEEPSEEK_API_KEY is not set." >&2
        echo "           set -Ux DEEPSEEK_API_KEY sk-..." >&2
        return 1
    end


<img src="/images/2026/claude-code-on-deepseek-one-command-and-the-base-u-body-2.jpg" alt="The Fish Function" srcset="            /assets/resized/480/claude-code-on-deepseek-one-command-and-the-base-u-body-2.jpg 480w,            /assets/resized/800/claude-code-on-deepseek-one-command-and-the-base-u-body-2.jpg 800w,            /assets/resized/1400/claude-code-on-deepseek-one-command-and-the-base-u-body-2.jpg 1400w,    " loading="lazy">


    # Claude Code appends /v1/messages, so the base must be the /anthropic
    # root. NOT /v1, that yields /v1/v1/messages and 404s.
    set -l base_url (set -q DEEPSEEK_BASE_URL; and echo $DEEPSEEK_BASE_URL; \
        or echo "https://api.deepseek.com/anthropic")
    set -l model_pro (set -q DEEPSEEK_OPUS_MODEL; and echo $DEEPSEEK_OPUS_MODEL; \
        or echo "deepseek-v4-pro")
    set -l model_flash (set -q DEEPSEEK_SONNET_MODEL; and echo $DEEPSEEK_SONNET_MODEL; \
        or echo "deepseek-v4-flash")

    switch "$argv[1]"
        case --ds-info
            echo "base url : $base_url"
            echo "opus  -> : $model_pro"
            echo "sonnet-> : $model_flash"
            echo "haiku -> : $model_flash"
            return 0
    end

    # `env` scopes these to the child only. Nothing leaks into this shell.
    env \
        ANTHROPIC_BASE_URL="$base_url" \
        ANTHROPIC_API_KEY="$DEEPSEEK_API_KEY" \
        ANTHROPIC_AUTH_TOKEN="$DEEPSEEK_API_KEY" \
        ANTHROPIC_DEFAULT_OPUS_MODEL="$model_pro" \
        ANTHROPIC_DEFAULT_SONNET_MODEL="$model_flash" \
        ANTHROPIC_DEFAULT_HAIKU_MODEL="$model_flash" \
        ANTHROPIC_DEFAULT_FABLE_MODEL="$model_flash" \
        ANTHROPIC_MODEL="" \
        CLAUDE_CODE_SUBAGENT_MODEL="inherit" \
        claude $argv
end

Those variables at the bottom get set on the child process only. When Claude Code exits, they are gone. claude on its own is completely untouched.

Save the file, and it works immediately:

claude-ds                    # interactive, on DeepSeek
claude-ds --model opus       # routes to deepseek-v4-pro
claude-ds -p "..." --resume  # any normal claude flag passes straight through
claude-ds --ds-info          # show the mapping it will use
claude                       # unchanged, still Anthropic

Because everything after the flag check goes into claude $argv, every flag you already know still works in this hacked DeepSeek version.

If You Don’t Use Fish

Most people don’t, and a shell change is not a casual afternoon. The same thing in bash or zsh is barely longer. Drop this in ~/.bashrc or ~/.zshrc:

claude-ds() {
  if [ -z "$DEEPSEEK_API_KEY" ]; then
    echo "claude-ds: DEEPSEEK_API_KEY is not set." >&2
    echo "           export DEEPSEEK_API_KEY=sk-..." >&2
    return 1
  fi

  local base_url="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/anthropic}"
  local model_pro="${DEEPSEEK_OPUS_MODEL:-deepseek-v4-pro}"
  local model_flash="${DEEPSEEK_SONNET_MODEL:-deepseek-v4-flash}"

  if [ "$1" = "--ds-info" ]; then
    printf 'base url : %s\nopus  -> : %s\nsonnet-> : %s\nhaiku -> : %s\n' \
      "$base_url" "$model_pro" "$model_flash" "$model_flash"
    return 0
  fi

  env \
    ANTHROPIC_BASE_URL="$base_url" \
    ANTHROPIC_API_KEY="$DEEPSEEK_API_KEY" \
    ANTHROPIC_AUTH_TOKEN="$DEEPSEEK_API_KEY" \
    ANTHROPIC_DEFAULT_OPUS_MODEL="$model_pro" \
    ANTHROPIC_DEFAULT_SONNET_MODEL="$model_flash" \
    ANTHROPIC_DEFAULT_HAIKU_MODEL="$model_flash" \
    ANTHROPIC_DEFAULT_FABLE_MODEL="$model_flash" \
    ANTHROPIC_MODEL="" \
    CLAUDE_CODE_SUBAGENT_MODEL="inherit" \
    claude "$@"
}

Then source ~/.zshrc and you have the same command. Same guarantee that plain claude is untouched.

The Slot Mapping, and the Problem I Invented

Claude Code has slots for models. The opus, sonnet, haiku, and fable aliases each resolve through their own ANTHROPIC_DEFAULT_*_MODEL variable. Haiku’s slot is also what background functionality runs on, so it fires constantly even when you never type haiku.

I mapped every slot just in case. Then I tested it, and discovered I did not need to:

claude-fable-5 => responded as deepseek-v4-flash

# claude-haiku-4-5-20251001  => responded as deepseek-v4-flash
# claude-sonnet-4-5-20250929 => responded as deepseek-v4-flash
# claude-opus-4-1-20250805   => responded as deepseek-v4-pro
# claude-fable-5             => responded as deepseek-v4-flash

DeepSeek’s endpoint maps Claude model names server side. Send it any claude-opus-* ID and it answers as deepseek-v4-pro. Send it anything else in the Claude family and you get deepseek-v4-flash. So the failure I was prepping for does not happen.

Send it something unknown and the error is helpful, which is more than Claude Code managed:

{"error":{"message":"The supported API model names are deepseek-v4-pro or
deepseek-v4-flash, but you passed totally-not-a-model.", ...}}

I kept the explicit mapping anyway, because I would rather decide which DeepSeek model each alias hits. It also means /model in the UI shows me deepseek-v4-pro instead of a Claude name that is being rewritten somewhere.

Does It Work?

Yes, and I checked both slots rather than assuming.

$ claude-ds -p "Reply with exactly: OK"
⚠ claude.ai connectors are disabled because ANTHROPIC_API_KEY or another
  auth source is set and takes precedence over your claude.ai login
OK

$ claude-ds --model opus -p "Reply with exactly: OPUS-OK"
OPUS-OK

That connectors warning is expected and it is scoped to the DeepSeek session only. It is Claude Code correctly telling you that an API key is in play. Since the key only exists inside that one child process, your normal claude still has its login and its connectors.

What This Is For

DeepSeek V4 is not Opus. Anyone selling you that is selling something. But it is cheap enough that I stop doing the mental arithmetic before every prompt, in an interface that I am already really used to.

The reason I kept going back to Claude Code even when I had a cheaper option running in another tool was never capability. I simply know where everything is there. Now the cheap path and the familiar path are the same path. I am still using both Claude Code with Claude models and Opencode, but now I can switch to a really cheap option and start on non-critical work quickly. Today I used it to run a deep research skill on a topic I am learning and it worked like a charm.

Anyway. My key still has most of its credits on it, which after two live test calls feels about right. Now I have to go find out what happens when I turn DeepSeek loose on a repo that does matter.

Stephan Miller

Written by

Kansas City Software Engineer and Author

Twitter | Github | LinkedIn

Updated