Use Qwen models in OpenCode for free by piggy-backing on your existing chat.qwen.ai browser session — no API key, no OAuth, no DashScope billing.
This plugin reads your active chat.qwen.ai session token straight from your local
Firefox cookie store and translates between OpenCode's OpenAI-style requests and
Qwen's internal web chat API. It adds two things Qwen's web backend doesn't give you on
its own: multi-account failover and a prompt-based tool-calling shim so agentic
Build mode works.
Why this exists: Qwen's official OAuth device-flow endpoint (
/api/v1/oauth2/token) currently returns504 Gateway Time-outfor many regions, which breaks the conventional OAuth plugins. The web chat backend (/api/v2/chat/completions), however, works fine — so this plugin uses that instead.
┌────────────┐ OpenAI-style ┌─────────────────┐ Qwen v2 web API ┌──────────────┐
│ OpenCode │ ───────────────▶ │ qwen-chat.js │ ──────────────────▶ │ chat.qwen.ai │
│ (TUI/CLI) │ ◀─────────────── │ (this plugin) │ ◀────────────────── │ backend │
└────────────┘ SSE deltas └─────────────────┘ SSE (phase data) └──────────────┘
│
│ reads session token
▼
~/Library/.../Firefox
cookies.sqlite
- Auth — On
opencode auth login, the plugin reads thetokencookie for*.qwen.aifrom Firefox'scookies.sqlite. No token is ever typed or stored by you. - Per request — It re-reads the freshest token from Firefox (cached briefly, and
treated as expired ~1 min before the JWT
exp), so as long as you stay logged in to chat.qwen.ai in Firefox, it keeps working. Token expiry is parsed from the JWT itself. It replays the full cookie jar for the profile (token,acw_tc,cna,tfstk,ssxmod_*, …), matching the real browser client and preserving WAF/load-balancer affinity cookies. - Request translation — Qwen's web endpoint hangs on multi-message threaded input,
so the plugin collapses the OpenAI conversation into a single user message per
request — but it preserves structure by labeling each turn (
[System],[User],[Assistant],[Tool Result]). It then creates a fresh chat session (/api/v2/chats/new) and posts to/api/v2/chat/completions. - Response translation — Qwen streams custom SSE events with
thinkingandanswerphases. The plugin drops the thinking phase and re-emits theanswerphase as standard OpenAIchat.completion.chunkdeltas, stopping as soon as Qwen signalsfinishedand ending with[DONE]. Plain chat streams token-by-token; tool turns are buffered (see below).
- No credentials are stored by the plugin or this repo. The token lives only in your browser; the plugin reads it at runtime.
- The cookie DB is copied to a temp file before reading (Firefox locks the live DB).
.gitignoreblocks*.har,*.sqlite,*token*,.env, and similar from ever being committed. Never commit a HAR capture or cookie file — they contain your live session.- Optional diagnostic logging is off by default and only writes when a flag file
(
~/.config/opencode/qwen-chat-debug) exists. When enabled it logs full prompts to~/.config/opencode/qwen-chat-debug.log(rotated at 1 MB) — delete the flag file to disable.
- OpenCode installed
- Firefox, logged in to https://chat.qwen.ai
sqlite3on yourPATH(preinstalled on macOS)- macOS (paths below are macOS; see notes for other OSes)
-
Drop the plugin into your OpenCode plugins directory (auto-loaded at startup):
mkdir -p ~/.config/opencode/plugins curl -fsSL https://raw.githubusercontent.com/schlambos/opencode-qwenchat-auth/main/qwen-chat.js \ -o ~/.config/opencode/plugins/qwen-chat.js
-
Register the provider in
~/.config/opencode/opencode.jsonc(add theqwen-chatblock underprovider): -
Log in to chat.qwen.ai in Firefox (if you aren't already).
-
Authenticate OpenCode:
opencode auth login
- Select Other
- Provider id:
qwen-chat - Choose Qwen Chat (multi-account, reads Firefox sessions)
- It authorizes automatically from your Firefox session(s).
-
Use it:
opencode --provider qwen-chat --model qwen3.6-plus
Or pick a Qwen model from the model switcher in the TUI.
Note: plugin changes only take effect on a fresh OpenCode start. Restart the TUI after installing or updating
qwen-chat.js.
- Stay logged in to chat.qwen.ai in Firefox. When Firefox's session token rotates, the plugin picks up the new one automatically on the next request.
- If you get a
401/auth error, just reload chat.qwen.ai in Firefox and retry.
If requests start failing with a waf_challenge error ("Qwen anti-bot (WAF) challenge…"),
Qwen's Aliyun WAF has flagged the traffic. This is not a quota or account problem, and
switching accounts won't help (it's device/IP-level). Fix it by:
- Open https://chat.qwen.ai in Firefox and send a message — solve any captcha.
- Reload the page so fresh affinity cookies (
acw_tc,tfstk, …) are written; the plugin picks them up automatically. - If it still fails, capture
bx-uaandbx-umidtokenfrom a real browser request and add them toqwen-accounts.json(see Optional: anti-bot headers).
| Model ID | Context | Notes |
|---|---|---|
qwen3.6-plus |
1M | Default, well-rounded |
qwen3.7-max |
1M | Most capable — best for agentic/tool work |
qwen3.6-max-preview |
256K | Preview flagship |
qwen3.5-plus |
1M | Solid general model |
qwen3.5-flash |
1M | Fastest |
qwen3.6-27b |
256K | Open-weight dense |
Model IDs match chat.qwen.ai's internal names and may change as Qwen updates them.
Models are addressed as
qwen-chat/<model>(e.g.qwen-chat/qwen3.6-plus).
Qwen's web API has no native function/tool calling, so agentic Build mode would normally fail. This plugin adds a prompt-based tool-call shim:
-
When OpenCode sends a
toolsarray, the plugin injects a forceful protocol into the prompt — the full tool list with per-argument schemas, a worked example, and a reminder — and instructs the model to reply with a fenced block using an escaping-freeARGformat:```tool_call TOOL: bash ARG command: find . -type f -name "*.js" | awk '{printf "%s\n", $0}' ARG description: list js files ```Values are written verbatim — no JSON string escaping. The plugin serializes them with
JSON.stringify, so theargumentsit emits to OpenCode are always valid JSON. (Raw JSON tool-call blocks are still accepted as a fallback.) -
The parsed call is emitted as OpenAI
tool_calls(finish_reason: "tool_calls"), so OpenCode executes the tool normally. -
Prior
tool_calls/toolresult messages are rendered back into the prompt ([Assistant]/[Tool Result]) so the model sees the loop across turns. -
Plain chat (no
tools) streams normally; tool turns are buffered (needed to parse the call before emitting it).
The injected protocol shows exact argument names (e.g. filePath, not path) and a
concrete example, which is what makes smaller models actually emit tool calls instead of
narrating shell commands as plain text. The parser is forgiving — it scans every fenced
block and the raw text, and accepts the tool name as TOOL: bash, name: bash, or a
bare bash line.
Model note: qwen3.7-max follows the protocol cleanly. qwen3.6-plus works but is
weaker and may occasionally prepend stray text; prefer qwen3.7-max for heavy agentic use.
Caveats: tool turns lose token-by-token streaming (buffered to parse); one tool call
per block in ARG mode (multiple calls still supported via a JSON array, or multiple
blocks); deeply nested object args rely on the model writing valid JSON on the value line.
When one Qwen account hits its free-usage limit, the plugin automatically fails over to another account so you keep working.
The plugin collects candidate accounts from two sources and de-duplicates them by the account id embedded in each session JWT:
-
Every Firefox profile — log a different Qwen account into each Firefox profile (
about:profiles→ Create a New Profile), and each profile'scookies.sqliteis read. -
A manual accounts file —
~/.config/opencode/qwen-accounts.json. Accepts either a bare array or a{ "tokens": [...] }object, and each entry may be a raw token string or{ "token": "...", "label": "...", "bxUa": "...", "bxUmidToken": "..." }:{ "tokens": ["<token-cookie-account-1>", "<token-cookie-account-2>"] }Manual tokens are sent token-only (no jar), since the extra cookies live in the browser profile, not the file. See
qwen-accounts.example.json. Never commit this file — it holds live session tokens (the.gitignorealready blocks it).
Qwen sits behind an Aliyun WAF. Today the cookie/token auth is enough, but if Qwen
starts enforcing its anti-bot layer you'll see WAF challenges (see [Troubleshooting]
(#troubleshooting-waf-challenges)). As a hedge you can capture those two request headers
from a real browser request (devtools → Network → a chat.qwen.ai request → Request
Headers) and add them top-level in qwen-accounts.json:
{ "tokens": ["…"], "bxUa": "<bx-ua value>", "bxUmidToken": "<bx-umidtoken value>" }When present they're attached to every request. They can also be set per-account on an object-form entry.
- Per-account rate-limit state is tracked in
~/.config/opencode/qwen-accounts-state.json(account id + cooldown timestamp only — no tokens). - On each request the plugin picks the least-recently-used account that isn't cooling down.
- A request fails over to the next account when it detects exhaustion via
classifyFailure:- HTTP
429with no quota wording → transient rate limit → 60-second cooldown. - HTTP
429/403/401whose body mentions quota/limit/exceeded/insufficient/free → quota → cooldown until the next UTC midnight (matching Qwen's daily free-tier reset). - HTTP
401/403without quota wording → treated as an auth error (no failover). - a quota/limit error in the first SSE chunk before any answer text → quota cooldown.
- HTTP
- If all accounts are cooling down, you get a
429telling you when the soonest one resets. - The response includes an
x-qwen-accountheader showing which account served it.
The plugin shows OpenCode toast notifications so you always know which account is active:
- info — "Using Qwen account:
<label>" the first time an account serves you (labels are the Firefox profile name, e.g. Firefox: default-release, or a name you set in the accounts file). - warning — "
<label>hit its limit — switching account…" on a failover. - success — "Using Qwen account:
<label>" after a successful failover to a new account. - error — when all accounts are rate-limited or every account failed.
Notifications only fire when the active account changes, so normal back-to-back messages stay quiet.
- Detection heuristics are best-effort — Qwen's exact "out of free usage" response isn't publicly documented, so classification keys off HTTP status + body/SSE text scanning.
- Quota cooldown assumes a daily UTC reset; tune in
markRateLimited()if Qwen differs. - Firefox only stores one live session per profile, so multiple accounts require multiple profiles and/or the accounts file.
- Single prompt per request: the conversation is collapsed into one role-labeled prompt per request (Qwen's web API hangs on threaded multi-message input). Long histories cost more tokens but work fine.
- Tool calling is prompt-based, not native: reliability depends on the model following the injected protocol (see Tool calling).
- Text only: image/audio passthrough is not implemented.
- Token usage is forwarded to OpenCode (mapped from Qwen's
usageblock), so token counts are reported; cost stays0since the web tier is free. - Firefox only: reads from Firefox's cookie store. Chrome/Safari would need a different cookie path and decryption (PRs welcome).
- Unofficial: this uses Qwen's private web API. It can break if Qwen changes it, and you are responsible for complying with Qwen's terms of service.
The cookie DB path is hard-coded for macOS Firefox:
~/Library/Application Support/Firefox/Profiles/<profile>/cookies.sqlite.
- Linux:
~/.mozilla/firefox/<profile>/cookies.sqlite - Windows:
%APPDATA%\Mozilla\Firefox\Profiles\<profile>\cookies.sqlite
Edit listFirefoxCookieDbs() in qwen-chat.js for your platform.
🤖 Agent install instructions (click to expand)
If you are an AI coding agent installing this plugin on the user's machine, follow these steps exactly. Do not print, log, or commit any token, cookie, or HAR file.
1. Install the plugin file
mkdir -p ~/.config/opencode/plugins
curl -fsSL https://raw.githubusercontent.com/schlambos/opencode-qwenchat-auth/main/qwen-chat.js \
-o ~/.config/opencode/plugins/qwen-chat.jsIf curl is unavailable or offline, write the contents of qwen-chat.js from this repo
to ~/.config/opencode/plugins/qwen-chat.js verbatim.
2. Register the provider in ~/.config/opencode/opencode.jsonc
Merge (do not overwrite) a qwen-chat entry into the top-level provider object using
the JSON block from the "How-to: install" section above. Preserve all existing providers,
plugins, and config. The file may be JSONC (comments allowed).
3. Verify the plugin loads (no secrets in output)
node --input-type=module --eval "
import p from '$HOME/.config/opencode/plugins/qwen-chat.js';
const h = await p({});
console.log('provider:', h.auth.provider);
console.log('methods:', h.auth.methods.map(m => m.type + ' / ' + m.label));
"Expect provider: qwen-chat and one oauth method. Do not print token values.
4. Confirm prerequisites with the user
- Firefox is installed and they are logged in to https://chat.qwen.ai.
sqlite3is on PATH (which sqlite3).
5. Tell the user to authenticate
Instruct them to run opencode auth login, choose Other, enter qwen-chat, and
select Qwen Chat (multi-account, reads Firefox sessions). It authorizes automatically.
Remind them to restart the TUI so the plugin loads.
6. Safety checks
- Before any
git add/commit, run a secret scan and abort if it matches:rg -i "eyJ|Bearer |token=ey|cookies\.sqlite" <staged files> - Ensure
.gitignoreblocks*.har,*.sqlite,*token*,.env. - Never commit
~/.config/opencode/opencode.jsonc(contains the user's full config).
MIT
{ "provider": { "qwen-chat": { "npm": "@ai-sdk/openai-compatible", "name": "Qwen Chat", "options": { "baseURL": "https://chat.qwen.ai/v1", "compatibility": "compatible" }, "models": { "qwen3.6-plus": { "name": "Qwen3.6 Plus", "limit": { "context": 1000000, "output": 65536 }, "cost": { "input": 0, "output": 0 } }, "qwen3.7-max": { "name": "Qwen3.7 Max", "limit": { "context": 1000000, "output": 65536 }, "cost": { "input": 0, "output": 0 } }, "qwen3.6-max-preview": { "name": "Qwen3.6 Max Preview", "limit": { "context": 262144, "output": 65536 }, "cost": { "input": 0, "output": 0 } }, "qwen3.5-plus": { "name": "Qwen3.5 Plus", "limit": { "context": 1000000, "output": 65536 }, "cost": { "input": 0, "output": 0 } }, "qwen3.5-flash": { "name": "Qwen3.5 Flash", "limit": { "context": 1000000, "output": 65536 }, "cost": { "input": 0, "output": 0 } }, "qwen3.6-27b": { "name": "Qwen3.6 27B", "limit": { "context": 262144, "output": 65536 }, "cost": { "input": 0, "output": 0 } } } } } }