You installed Qwen Code, ran qwen, pointed it at Model Studio or OpenRouter, and got a wall of 401 Invalid API-key provided. The key works in curl. It works in another tool. But the CLI rejects it. Welcome to the most common Qwen Code support thread on the internet.
The frustrating part is that almost none of these 401s are about a bad key. They're about which endpoint the key is being sent to, which environment variable actually wins at runtime, and a config schema where one wrong word makes your provider vanish silently. This guide walks the auth model end to end — Model Studio, OpenRouter, and local — and then fixes the 401s by cause.
The four ways Qwen Code authenticates
Qwen Code supports several auth routes, and picking the wrong one for your situation is half the battle:
- Qwen OAuth — browser login to a qwen.ai account. The free tier here was reportedly discontinued in 2026, so this is no longer the default path for most people.
- Alibaba Cloud Coding Plan — a subscription with its own dedicated endpoint, separate from standard pay-as-you-go keys.
- API Key — the workhorse. OpenAI-compatible, Anthropic, or Google GenAI protocols, covering Model Studio/DashScope, OpenRouter, ModelScope, and self-hosted/local servers.
Most of this guide is about the API-key route, because that's where the 401s live.
How config actually resolves
Qwen Code reads config from ~/.qwen/settings.json (C:\Users\<user>\.qwen\settings.json on Windows). The fields that matter for auth are modelProviders, env, security.auth.selectedType, and model.name.
But settings.json isn't the only source. Config resolves in this priority order, highest to lowest:
- CLI flags
- System/exported environment variables
.envfile (search order:.qwen/.env→.env→~/.qwen/.env→~/.env)- The
envfield insidesettings.json
This ordering is the single most important thing to understand for debugging 401s. A
OPENAI_API_KEYyou exported in your shell three weeks ago overrides the key in your settings.json. You edit the file, restart, and nothing changes — because the stale exported variable still wins.
When in doubt, check what's actually in your environment (env | grep -i openai on macOS/Linux) before you touch settings.json.
The protocol-key-naming gotcha
This one burns everybody once. Inside modelProviders, the top-level key is the protocol/auth type, and it must be exactly one of openai, anthropic, or gemini. That key decides which SDK handles the request:
| Protocol key | SDK used | Use for |
|---|---|---|
openai | OpenAI Node SDK | Model Studio, OpenRouter, Ollama, LM Studio, vLLM, any OpenAI-compatible endpoint |
anthropic | Anthropic SDK | Anthropic-protocol endpoints |
gemini | Google GenAI SDK | Gemini-protocol endpoints |
If you invent a key — say openai-custom, or dashscope, or openrouter — the entire entry is silently skipped. No error. No warning. The model just never shows up in the /model picker, and you'll spend an hour wondering why. If your model is missing after editing settings.json, the protocol key is the first thing to check.
A couple of related rules: each provider entry needs an id (the model identifier sent to the API) and an envKey. And envKey holds the name of an environment variable, not the key value itself. Putting your literal sk-... string into envKey is a classic misconfiguration. Optional fields include name (display name, falls back to id), description, baseUrl (defaults to the provider's official URL), and generationConfig.
Two more sharp edges worth knowing:
- Duplicate model IDs within the same authType aren't supported — the first occurrence wins, later ones are skipped with warnings.
- A selected provider's
generationConfigreplaces all lower-layer config atomically. The provider layer is impermeable: fields you don't define becomeundefinedrather than inheriting from the rest of settings.
Model Studio / DashScope: get the region right
For OpenAI-compatible API-key auth, the relevant environment variables are OPENAI_API_KEY (auth), OPENAI_BASE_URL (endpoint override), and OPENAI_MODEL (model selection). Protocol-specific variants exist too: ANTHROPIC_API_KEY and GEMINI_API_KEY.
Model Studio (DashScope) is OpenAI-compatible, but it publishes different base URLs per region, and this is the number-one cause of 401s with Alibaba's service:
| Region | OpenAI-compatible base URL |
|---|---|
| China / Beijing | https://dashscope.aliyuncs.com/compatible-mode/v1 |
| Singapore / International | https://dashscope-intl.aliyuncs.com/compatible-mode/v1 |
| US / Virginia | https://dashscope-us.aliyuncs.com/compatible-mode/v1 |
| Hong Kong | https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1 |
A key created in the international (Singapore) console used against the China endpoint is rejected with a 401. The key is bound to the region's account where it was created — you have to pair it with the matching regional base URL. If your key is "definitely correct" and still 401s, this is almost certainly why.
A minimal OpenAI-compatible settings.json looks like this:
{
"modelProviders": {
"openai": [
{
"id": "qwen3-coder-plus",
"baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
"envKey": "DASHSCOPE_API_KEY"
}
]
},
"env": {
"DASHSCOPE_API_KEY": "your-key-here"
},
"security": { "auth": { "selectedType": "openai" } },
"model": { "name": "qwen3-coder-plus" }
}
Note selectedType matches the protocol key (openai), and model.name matches the id. One caveat on the variable name: the env var for the Model Studio key is reported inconsistently across sources — DASHSCOPE_API_KEY, BAILIAN_API_KEY, and for Coding Plan BAILIAN_CODING_PLAN_API_KEY all show up. Verify the exact name against the current official docs for your account; whatever it is, envKey must point to that same name.
Standard key vs. Coding Plan
Standard pay-as-you-go DashScope keys and Alibaba Cloud Coding Plan keys use different endpoints. The Coding Plan uses a dedicated base URL (reported as https://coding.dashscope.aliyuncs.com/v1 for China, with an international equivalent). Use a Coding Plan endpoint with a standard key — or the reverse — and you get 401s. Match the key type to its endpoint.
Available model IDs (e.g., qwen3-coder-plus, qwen3-coder-next, and broader qwen-max/plus/flash, plus partner models like GLM and Kimi via the Coding Plan) vary by region and endpoint. Confirm what's live in your console rather than copying an ID from a blog.
OpenRouter: global, free-ish, and OpenAI-compatible
OpenRouter is the easiest escape hatch if you're hitting the China region block, because it's globally reachable and OpenAI-compatible. The setup is pure environment variables:
export OPENAI_API_KEY=sk-or-v1-... # your OpenRouter key
export OPENAI_BASE_URL=https://openrouter.ai/api/v1
export OPENAI_MODEL=qwen/qwen3-coder:free # provider/model[:tag] format
Note the model name format: provider/model[:tag], not a bare model ID.
Two OpenRouter-specific failures to watch for:
- 401 / "Incorrect API key provided" while it's actually hitting api.openai.com. This happens when the OpenAI provider ignores your configured base URL and falls back to OpenAI's default. The fix is to ensure
OPENAI_BASE_URLis set to exactlyhttps://openrouter.ai/api/v1and to clear any conflicting env vars or settings entries that might override it. (Remember the resolution order — an exported var beats settings.json.) - "404 no tool use." Qwen Code is agentic and leans on tool/function calls. If OpenRouter routes you to a provider variant that doesn't support tool calling, the request fails. Pin a tool-capable route.
Also budget for OpenRouter's own free-tier rate limits — reported as roughly 20 requests/minute and around 200 requests/day per model. Those are OpenRouter's limits, not Qwen Code's, and they change.
Local: Ollama, LM Studio, vLLM
Fully local works because these servers all speak OpenAI-compatible HTTP. Just point the base URL at the local endpoint:
| Server | Local base URL |
|---|---|
| Ollama | http://localhost:11434/v1 |
| LM Studio | http://localhost:1234/v1 |
| vLLM | http://localhost:8000/v1 |
Set OPENAI_BASE_URL to the local endpoint and OPENAI_MODEL to the served model. For servers that don't require auth, any non-empty placeholder value works for the API key — the SDK just needs something in the field.
If you want local-first economics without giving up a cloud fallback, you can also point OPENAI_BASE_URL at a local-first AI gateway like Wide Area AI, which presents an OpenAI-compatible endpoint that serves requests from your own hardware first and fails over to cloud providers (OpenAI/Gemini/Claude) when a local node is unavailable. Requests served locally carry zero per-token cost, and because the gateway is OpenAI-compatible the same OPENAI_BASE_URL/OPENAI_MODEL setup applies unchanged.
Keeping your key out of git
Because envKey references a variable name and the real key lives in env or a .env file, the safe pattern is to put the key in a .env that's gitignored — or in exported shell variables — and keep settings.json free of literal secrets. Given the .env search order, .qwen/.env is a clean per-project home for it.
Bottom line
Qwen Code 401s are rarely about a bad key. Work the causes in order: (1) confirm the protocol key in modelProviders is exactly openai/anthropic/gemini, or the provider is silently dropped; (2) confirm envKey names a variable and the key lives in env/.env; (3) check the config resolution order — a stale exported OPENAI_API_KEY will quietly override your settings; (4) for Model Studio, match the key's region to its base URL, and don't mix standard and Coding Plan endpoints; (5) for OpenRouter, set OPENAI_BASE_URL to exactly https://openrouter.ai/api/v1 and pin a tool-capable model. Get those five right and the 401s disappear. The tools and quotas move fast, so verify current model IDs, env-var names, and free-tier terms in the live console before you commit to a setup.