Skip to content

Commit ae336ff

Browse files
feat: plugin OAuth Device Flow funcionando com method code
Plugin de autenticação OAuth para OpenCode que integra modelos Qwen através de conta qwen.ai usando Device Flow (RFC 8628). Funcionalidades: - Login via Device Flow com PKCE - Importação de credenciais do qwen-code (~/.qwen/oauth_creds.json) - Refresh automático de tokens - Suporte aos modelos qwen3-coder-plus, qwen3-coder-flash, qwen-coder-plus
0 parents  commit ae336ff

14 files changed

Lines changed: 1410 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
*.log
4+
.DS_Store

README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Qwen OAuth Plugin for OpenCode
2+
3+
Authenticate OpenCode CLI with your qwen.ai account to use Qwen3-Coder models with **2,000 free requests per day**!
4+
5+
## Features
6+
7+
- 🔐 **OAuth Authentication** - Sign in with your qwen.ai account
8+
- 🆓 **Free Tier** - 2,000 requests/day, 60 requests/minute (no token limits!)
9+
- 🔄 **Auto-refresh** - Automatic token refresh
10+
- 🔗 **qwen-code compatible** - Reuses credentials from qwen-code CLI
11+
-**1M Context** - Access to models with 1M token context windows
12+
13+
## Quick Start
14+
15+
### 1. Add the plugin to your OpenCode config
16+
17+
Create or edit `~/.config/opencode/opencode.json`:
18+
19+
```json
20+
{
21+
"$schema": "https://opencode.ai/config.json",
22+
"plugin": ["opencode-qwen-auth"]
23+
}
24+
```
25+
26+
### 2. Authenticate
27+
28+
```bash
29+
opencode auth login
30+
```
31+
32+
Choose the **Qwen** provider and select **OAuth with Qwen (qwen.ai account)**.
33+
34+
### 3. Start using Qwen models
35+
36+
```bash
37+
opencode --model qwen/qwen3-coder-plus
38+
```
39+
40+
## Available Models
41+
42+
| Model | Context | Output | Description |
43+
|-------|---------|--------|-------------|
44+
| `qwen3-coder-plus` | 1M tokens | 64K tokens | Most capable coding model |
45+
| `qwen3-coder-flash` | 1M tokens | 64K tokens | Faster responses |
46+
| `qwen-coder-plus` | 128K tokens | 32K tokens | Standard coding model |
47+
48+
## Alternative: Import from qwen-code
49+
50+
If you already use [qwen-code](https://github.com/QwenLM/qwen-code), this plugin will automatically detect and reuse your existing credentials from `~/.qwen/oauth_creds.json`.
51+
52+
Just run:
53+
54+
```bash
55+
# First, authenticate with qwen-code
56+
qwen # This will prompt for OAuth login
57+
58+
# Then OpenCode will automatically use those credentials
59+
opencode --model qwen/qwen3-coder-plus
60+
```
61+
62+
## Configuration
63+
64+
### Full config example
65+
66+
```json
67+
{
68+
"$schema": "https://opencode.ai/config.json",
69+
"plugin": ["opencode-qwen-auth"],
70+
"provider": {
71+
"qwen": {
72+
"models": {
73+
"qwen3-coder-plus": {
74+
"name": "Qwen3 Coder Plus (OAuth)",
75+
"limit": {
76+
"context": 1048576,
77+
"output": 65536
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
```
85+
86+
### Environment Variables
87+
88+
| Variable | Description |
89+
|----------|-------------|
90+
| `OPENCODE_QWEN_DEBUG` | Set to `1` to enable debug logging |
91+
92+
## Quota Information
93+
94+
| Plan | Rate Limit | Daily Limit |
95+
|------|------------|-------------|
96+
| Free (OAuth) | 60 requests/min | 2,000 requests/day |
97+
98+
## Local Development
99+
100+
```bash
101+
# Clone the repository
102+
git clone https://github.com/YOUR_USERNAME/opencode-qwen-auth.git
103+
cd opencode-qwen-auth
104+
105+
# Install dependencies
106+
bun install
107+
108+
# Build
109+
bun run build
110+
111+
# Link to OpenCode config
112+
# Add to ~/.config/opencode/opencode.json:
113+
{
114+
"plugin": ["file:///absolute/path/to/opencode-qwen-auth/dist/index.js"]
115+
}
116+
```
117+
118+
## How It Works
119+
120+
1. **OAuth Flow**: Opens your browser to qwen.ai for authentication
121+
2. **Token Storage**: Saves credentials to `~/.qwen/oauth_creds.json`
122+
3. **Auto-refresh**: Refreshes tokens automatically when they expire
123+
4. **API Proxy**: Routes OpenCode requests through Qwen's chat API
124+
125+
## Troubleshooting
126+
127+
### Port 14561 already in use
128+
129+
The plugin uses port 14561 for the OAuth callback. If it's busy:
130+
131+
1. Check for other processes: `lsof -i :14561`
132+
2. Kill the process or wait for it to finish
133+
134+
### Token expired
135+
136+
The plugin automatically refreshes tokens. If issues persist:
137+
138+
```bash
139+
# Remove old credentials
140+
rm ~/.qwen/oauth_creds.json
141+
142+
# Re-authenticate
143+
opencode auth login
144+
```
145+
146+
### Rate limit exceeded
147+
148+
If you hit the daily limit (2,000 requests):
149+
- Wait until midnight UTC for quota reset
150+
- Consider using API keys for higher limits
151+
152+
## Related Projects
153+
154+
- [qwen-code](https://github.com/QwenLM/qwen-code) - Official Qwen coding CLI
155+
- [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) - Gemini OAuth plugin (inspiration for this project)
156+
- [qwen-code-oai-proxy](https://github.com/aptdnfapt/qwen-code-oai-proxy) - Proxy for using Qwen with any OpenAI-compatible client
157+
158+
## License
159+
160+
MIT

bun.lock

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Apenas exportar o plugin principal
2+
// NÃO exportar funções utilitárias pois o OpenCode trata todas as exportações como plugins
3+
export { QwenAuthPlugin, default } from "./src/index";

package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "opencode-qwen-auth",
3+
"version": "1.0.0",
4+
"description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen3-Coder models with your qwen.ai account",
5+
"module": "index.ts",
6+
"type": "module",
7+
"scripts": {
8+
"build": "bun build ./src/index.ts --outdir ./dist --target node --format esm && bun build ./src/cli.ts --outdir ./dist --target node --format esm",
9+
"dev": "bun run --watch src/index.ts",
10+
"typecheck": "tsc --noEmit"
11+
},
12+
"keywords": [
13+
"opencode",
14+
"qwen",
15+
"qwen-code",
16+
"qwen3-coder",
17+
"oauth",
18+
"authentication",
19+
"ai",
20+
"llm",
21+
"opencode-plugins"
22+
],
23+
"author": "",
24+
"license": "MIT",
25+
"repository": {
26+
"type": "git",
27+
"url": "https://github.com/YOUR_USERNAME/opencode-qwen-auth"
28+
},
29+
"dependencies": {
30+
"open": "^10.1.0"
31+
},
32+
"devDependencies": {
33+
"@opencode-ai/plugin": "^1.1.48",
34+
"@types/node": "^22.0.0",
35+
"bun-types": "^1.1.0",
36+
"typescript": "^5.6.0"
37+
},
38+
"files": [
39+
"index.ts",
40+
"src",
41+
"README.md"
42+
],
43+
"engines": {
44+
"node": ">=20.0.0"
45+
}
46+
}

src/cli.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Qwen Auth CLI Helper
4+
*
5+
* This script helps with manual authentication when the automatic
6+
* OAuth flow doesn't work (e.g., in SSH sessions, containers, etc.)
7+
*/
8+
9+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10+
import { homedir } from 'node:os';
11+
import { join } from 'node:path';
12+
import { createInterface } from 'node:readline';
13+
14+
const CREDS_PATH = join(homedir(), '.qwen', 'oauth_creds.json');
15+
16+
const rl = createInterface({
17+
input: process.stdin,
18+
output: process.stdout,
19+
});
20+
21+
function question(prompt: string): Promise<string> {
22+
return new Promise((resolve) => {
23+
rl.question(prompt, resolve);
24+
});
25+
}
26+
27+
async function main() {
28+
console.log('\nQwen Auth CLI Helper\n');
29+
console.log('This tool helps you set up Qwen authentication manually.\n');
30+
31+
// Check for existing credentials
32+
if (existsSync(CREDS_PATH)) {
33+
const data = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
34+
console.log('Existing credentials found at:', CREDS_PATH);
35+
36+
if (data.access_token) {
37+
console.log('Access token: Present');
38+
console.log('Email:', data.email || 'Not set');
39+
console.log('Updated at:', data.updated_at ? new Date(data.updated_at).toISOString() : 'Unknown');
40+
41+
const overwrite = await question('\nOverwrite existing credentials? (y/N): ');
42+
if (overwrite.toLowerCase() !== 'y') {
43+
console.log('\nKeeping existing credentials. Exiting.');
44+
rl.close();
45+
return;
46+
}
47+
}
48+
}
49+
50+
console.log('\nInstructions:');
51+
console.log('1. Open https://chat.qwen.ai in your browser');
52+
console.log('2. Sign in with your account');
53+
console.log('3. Open Developer Tools (F12) -> Network tab');
54+
console.log('4. Make any chat request');
55+
console.log('5. Find a request to chat.qwen.ai');
56+
console.log('6. Copy the "Authorization" header value (starts with "Bearer ...")');
57+
console.log('');
58+
59+
const token = await question('Paste your Bearer token (or just the token without "Bearer "): ');
60+
61+
if (!token.trim()) {
62+
console.log('No token provided. Exiting.');
63+
rl.close();
64+
return;
65+
}
66+
67+
// Clean up the token
68+
let accessToken = token.trim();
69+
if (accessToken.toLowerCase().startsWith('bearer ')) {
70+
accessToken = accessToken.slice(7);
71+
}
72+
73+
const email = await question('Email (optional, press Enter to skip): ');
74+
75+
// Save credentials
76+
const dir = join(homedir(), '.qwen');
77+
if (!existsSync(dir)) {
78+
mkdirSync(dir, { recursive: true });
79+
}
80+
81+
const credentials = {
82+
access_token: accessToken,
83+
email: email.trim() || undefined,
84+
updated_at: Date.now(),
85+
};
86+
87+
writeFileSync(CREDS_PATH, JSON.stringify(credentials, null, 2));
88+
89+
console.log('\nCredentials saved to:', CREDS_PATH);
90+
console.log('\nYou can now use OpenCode with Qwen models:');
91+
console.log(' opencode --model qwen/qwen3-coder-plus');
92+
93+
rl.close();
94+
}
95+
96+
main().catch((error) => {
97+
console.error('Error:', error);
98+
rl.close();
99+
process.exit(1);
100+
});

0 commit comments

Comments
 (0)