Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
 
 

Repository files navigation

OpenCode Qwen OAuth

npm version License: MIT Built with Bun

Fork of mseptiaan/opencode-qwencode-oauth v1.0.3

Qwen OAuth authentication plugin for OpenCode with PKCE device-flow auth, multi-account rotation, proactive token refresh, and automatic OpenAI Responses API ↔ Chat Completions translation.

What It Does

When installed as an OpenCode plugin, this intercepts all fetch requests made to the qwen provider and:

  1. Authenticates via Qwen's OAuth 2.0 Device Authorization flow with PKCE (S256 code challenge) against chat.qwen.ai
  2. Manages tokens — proactively refreshes access tokens before expiry (default 300s window), handles invalid_grant/invalid_token by marking accounts for re-auth
  3. Rotates accounts across multiple Qwen logins using one of three strategies
  4. Rate-limits itself — detects 429/5xx responses, backs off per reason tier (QUOTA_EXHAUSTED, RATE_LIMIT_EXCEEDED, SERVER_ERROR), respects retry-after headers
  5. Translates APIs — rewrites OpenAI Responses API (/responses) requests to Qwen's Chat Completions (/chat/completions) and transforms responses back, including SSE streaming
  6. Sanitizes headers — strips OpenAI-Beta and x-session-affinity, sets DashScope-specific headers

Architecture

OpenCode ──fetch()──> QwenOAuthPlugin ──> Qwen API (portal.qwen.ai/v1)
                          │
                          ├── OAuth: chat.qwen.ai (PKCE device flow)
                          ├── Accounts: ~/.config/opencode/qwen-auth-accounts.json
                          ├── Rotation: health scores + token bucket + LRU
                          └── Transform: /responses ↔ /chat/completions

Quick Start

{
  "plugin": ["opencode-qwencode-oauth"],
  "provider": {
    "qwen": {
      "npm": "@ai-sdk/openai",
      "options": {
        "baseURL": "https://portal.qwen.ai/v1",
        "compatibility": "strict"
      }
    }
  }
}

Then run /connect in OpenCode. A browser window opens — authenticate with Qwen, and the plugin polls the device token endpoint until approved.

CLI Install

npx opencode-qwencode-oauth install
bunx opencode-qwencode-oauth install --global

Creates a backup of your existing config, then writes the plugin entry and default qwen provider to opencode.json.

Rotation Strategies

Configured via rotation_strategy in .opencode/qwen.json or env QWEN_ROTATION_STRATEGY.

Strategy Behavior
hybrid (default) Multi-signal: health score ×2 + token balance (0–500) + LRU freshness (0–360). Filters rate-limited, score <50, and token-exhausted accounts.
round-robin Cycles through accounts. Skips rate-limited/reauth-required.
sequential Prefers activeIndex, walks forward.

Health Score System

Each account starts at 70/100. Scores recover passively at +2/hour. Events adjust:

  • Success: +1
  • Rate-limit (429): −10
  • Failure (5xx, auth): −20

Accounts below min_usable (default 50) are deprioritized by hybrid selection.

Token Bucket

Default 50 tokens per account, regenerating at 6/minute. Each request costs 1 token. Prevents hammering Qwen's API before their rate limiter kicks in.

Proactive Token Refresh

Enabled by default (proactive_refresh: true). Refreshes the access token when it's within refresh_window_seconds (default 300) of expiry. The refreshed token is persisted both to local storage and synced back to OpenCode via client.auth.set().

API Translation

OpenCode uses the OpenAI Responses API (/responses). Qwen speaks Chat Completions (/chat/completions). This plugin bridges them:

Request (/responses/chat/completions):

  • inputmessages
  • instructionssystem message prepended
  • max_output_tokensmax_tokens
  • input_image/input_audio/input_textimage_url/text content parts
  • developer role → system role
  • function_call_outputtool role messages
  • Tool format normalization (flat → nested function object)
  • Injects stream_options: { include_usage: true } and vl_high_resolution_images: true

Response (Chat Completions → Responses):

  • Non-streaming: Rewrites choices[0].message into output[].message with output_text parts, function_call items, and usage mapping
  • SSE streaming: Real-time transform via TransformStream — emits response.created, response.output_item.added, response.content_part.added, response.output_text.delta, response.function_call_arguments.delta, and response.completed events

Configuration Sources

Layered, each overriding the previous:

  1. Defaults (hardcoded in src/constants.ts)
  2. User config~/.config/opencode/qwen.json (Linux/macOS) or %APPDATA%/opencode/qwen.json (Windows)
  3. Project config.opencode/qwen.json in the project directory
  4. Environment variablesQWEN_OAUTH_CLIENT_ID, QWEN_OAUTH_BASE_URL, QWEN_API_BASE_URL, QWEN_ROTATION_STRATEGY, QWEN_PROACTIVE_REFRESH, QWEN_REFRESH_WINDOW_SECONDS, QWEN_MAX_RATE_LIMIT_WAIT_SECONDS, QWEN_QUIET_MODE, QWEN_PID_OFFSET_ENABLED

Full Config Schema (zod-validated)

{
  client_id: string,                    // default: "f0304373b74a44d2b584a3fb70ca9e56"
  oauth_base_url: string,               // default: "https://chat.qwen.ai"
  base_url: string,                     // default: "https://portal.qwen.ai/v1"
  rotation_strategy: "hybrid" | "round-robin" | "sequential",
  proactive_refresh: boolean,           // default: true
  refresh_window_seconds: number,       // default: 300
  max_rate_limit_wait_seconds: number,  // default: 300
  quiet_mode: boolean,                  // default: false
  pid_offset_enabled: boolean,          // default: false
  health_score?: {
    initial: number,                    // default: 70
    success_reward: number,             // default: 1
    rate_limit_penalty: number,         // default: -10
    failure_penalty: number,            // default: -20
    recovery_rate_per_hour: number,     // default: 2
    min_usable: number,                 // default: 50
  },
  token_bucket?: {
    max_tokens: number,                 // default: 50
    regeneration_rate_per_minute: number // default: 6
  }
}

Persistence

All state stored in the OpenCode config directory:

File Purpose
qwen-auth-accounts.json Refresh tokens, access tokens, expiry, rate-limit reset timestamps, per-account health metrics
qwen-auth-tracker-state.json Health score tracker and token bucket state

Writes are atomic: temp file + rename. File locking via mkdir-based advisory locks (10s stale threshold, 5 retries with exponential backoff).

OAuth Flow Details

  1. Client generates a PKCE code verifier (32 random bytes, base64url) and S256 challenge
  2. POST to /api/v1/oauth2/device/code with client_id, scope, code_challenge, code_challenge_method=S256
  3. Returns device_code, user_code, verification_uri, verification_uri_complete
  4. Polls /api/v1/oauth2/token with grant_type=urn:ietf:params:oauth:grant-type:device_code until:
    • Success: receives access_token, refresh_token, expires_in, resource_url
    • authorization_pending: retry after interval
    • slow_down: increase interval by 5s
    • expired_token: fail
  5. Token refresh: POST to same endpoint with grant_type=refresh_token

Rate Limit Backoff Tiers

Reason Detection Backoff Sequence
QUOTA_EXHAUSTED x-error-code header 60s → 300s → 1800s
RATE_LIMIT_EXCEEDED x-error-code header 30s → 60s
SERVER_ERROR 5xx status or header 20s → 40s
UNKNOWN Fallback 60s

retry-after-ms and retry-after response headers take precedence over tier defaults.

Development

bun install
tsc --noEmit       # typecheck
bun test           # run 135+ tests
bun test --watch   # watch mode
bun test:e2e       # end-to-end against mock server
bun run build      # tsc build to dist/
bun run lint       # biome check

CI runs on ubuntu, macOS, and Windows via GitHub Actions.

Project Structure

src/
├── cli/install.ts       # Interactive installer CLI
├── constants.ts         # Qwen API endpoints & defaults
├── plugin.ts            # Plugin entry point & fetch interceptor
├── plugin/
│   ├── account.ts       # Account storage, selection, health metrics
│   ├── auth.ts          # OAuth type guards & expiry checks
│   ├── config/          # Schema (zod), loader, env overrides
│   ├── logger.ts        # Namespaced debug logger
│   ├── rotation.ts      # Health scores, token bucket, hybrid selection
│   ├── token.ts         # Token refresh orchestration
│   └── types.ts         # Plugin type definitions
├── qwen/
│   └── oauth.ts         # PKCE device flow & token refresh HTTP
├── transform/
│   ├── header.ts        # Header normalization & DashScope injection
│   ├── request.ts       # Responses → Chat Completions transform
│   ├── response.ts      # Chat Completions → Responses (non-streaming)
│   └── sse.ts           # SSE stream transform (streaming)
└── types/
    └── prompts.d.ts     # Module declaration for prompts package
test/
├── account.test.ts
├── auth.test.ts
├── cli.test.ts
├── e2e.test.ts
├── migration.test.ts
├── mock-server/server.ts
├── rotation.test.ts
├── token.test.ts
├── transform.test.ts
└── url.test.ts

License

MIT

About

Qwen OAuth authentication plugin for OpenCode with multi-account rotation and API translation

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages