opencode-qwen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 20
17
+ - run: npm install
18
+
19
+ publish-npm:
20
+ needs: build
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: actions/setup-node@v4
25
+ with:
26
+ node-version: 20
27
+ registry-url: https://registry.npmjs.org/
28
+ - run: npm publish
29
+ env:
30
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # Qwen Authentication Plugin for OpenCode
2
+
3
+ This plugin provides seamless authentication integration with the Qwen AI API provider for OpenCode, handling token validation, refresh, and secure credential management.
4
+
5
+ ## Features
6
+
7
+ - **Automatic Token Management**: Validates and refreshes tokens automatically
8
+ - **Secure Credential Handling**: Integrates with environment variables
9
+ - **Token Refresh Support**: Handles access token renewal using refresh tokens
10
+ - **Custom Tools**: Provides dedicated tools for Qwen API interactions
11
+ - **Logging & Monitoring**: Comprehensive logging for debugging and monitoring
12
+ - **Type Safety**: Full TypeScript support with Zod validation
13
+
14
+ ## Installation
15
+
16
+ 1. Place the plugin files in your OpenCode plugin directory:
17
+ ```
18
+ .opencode/
19
+ ├── plugins/
20
+ │ └── qwen-auth.ts
21
+ └── package.json
22
+ ```
23
+
24
+ 2. Install dependencies:
25
+ ```bash
26
+ cd .opencode && bun install
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ Set the following environment variables in your environment or `.env` file:
32
+
33
+ ```bash
34
+ # Required: Your Qwen API access token
35
+ QWEN_API_KEY=your_access_token_here
36
+
37
+ # Optional: Your refresh token for automatic token renewal
38
+ QWEN_REFRESH_TOKEN=your_refresh_token_here
39
+
40
+ # Optional: Enable/disable automatic token refresh (default: true)
41
+ QWEN_AUTO_REFRESH=true
42
+
43
+ # Optional: Enable/disable token validation on startup (default: true)
44
+ QWEN_VALIDATE_ON_STARTUP=true
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Custom Tools
50
+
51
+ The plugin provides the following custom tools:
52
+
53
+ #### `qwen.validate-token`
54
+ Validate your Qwen API access token.
55
+
56
+ ```javascript
57
+ // Example usage
58
+ const result = await qwen.validate-token({
59
+ token: "optional_token_to_validate" // Uses current token if not provided
60
+ })
61
+ ```
62
+
63
+ #### `qwen.refresh-token`
64
+ Refresh your access token using the refresh token.
65
+
66
+ ```javascript
67
+ // Example usage
68
+ const result = await qwen.refresh-token({
69
+ refreshToken: "optional_refresh_token" // Uses stored refresh token if not provided
70
+ })
71
+ ```
72
+
73
+ #### `qwen.list-models`
74
+ List all available Qwen models (requires valid authentication).
75
+
76
+ ```javascript
77
+ // Example usage
78
+ const models = await qwen.list-models({})
79
+ console.log(`Available models: ${models.data.length}`)
80
+ ```
81
+
82
+ ### Automatic Token Management
83
+
84
+ The plugin automatically handles:
85
+
86
+ - **Session Validation**: Validates tokens when a new session starts
87
+ - **Periodic Refresh**: Checks token validity during idle periods
88
+ - **API Call Interception**: Ensures valid tokens before Qwen API calls
89
+ - **Error Recovery**: Attempts token refresh on validation failures
90
+
91
+ ## API Endpoints
92
+
93
+ The plugin integrates with the following Qwen API endpoints:
94
+
95
+ - `GET /validate` - Token validation
96
+ - `POST /refresh` - Token refresh
97
+ - `GET /models` - List available models
98
+
99
+ ## Security Considerations
100
+
101
+ - Store API keys and refresh tokens in environment variables, not in code
102
+ - The plugin never logs sensitive credential information
103
+ - Token validation is performed over secure HTTPS connections
104
+ - Consider rotating API keys regularly for enhanced security
105
+
106
+ ## Troubleshooting
107
+
108
+ ### Common Issues
109
+
110
+ 1. **"No valid Qwen authentication token available"**
111
+ - Ensure `QWEN_API_KEY` is set correctly
112
+ - Check if the token has expired and refresh if needed
113
+
114
+ 2. **"No refresh token available"**
115
+ - Set `QWEN_REFRESH_TOKEN` environment variable
116
+ - Obtain a refresh token from your Qwen API dashboard
117
+
118
+ 3. **"Failed to fetch models"**
119
+ - Validate your token first using `qwen.validate-token`
120
+ - Check network connectivity to `qwen.aikit.club`
121
+
122
+ ### Debug Logging
123
+
124
+ Enable debug logging to troubleshoot issues:
125
+
126
+ ```bash
127
+ # Set log level for more detailed output
128
+ export DEBUG=qwen-auth:*
129
+ ```
130
+
131
+ ## Development
132
+
133
+ The plugin is built with TypeScript and includes:
134
+
135
+ - **Type Safety**: Full TypeScript definitions for all API interactions
136
+ - **Zod Validation**: Runtime validation for all inputs and outputs
137
+ - **Structured Logging**: Consistent logging format for debugging
138
+ - **Error Handling**: Comprehensive error handling and recovery
139
+
140
+ ## License
141
+
142
+ This plugin is provided as-is for use with OpenCode and the Qwen API.
package/auth.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Qwen Provider Authentication Handler for OpenCode
3
+ *
4
+ * This module handles authentication integration with OpenCode's /connect command
5
+ * and provides secure credential management.
6
+ */
7
+
8
+ import { z } from 'zod'
9
+ import type { AuthHandler, Credential } from '@opencode-ai/plugin'
10
+ import { validateQwenApiKey, createQwenProvider, fetchQwenModels } from './index'
11
+
12
+ // Credential validation schema
13
+ const QwenCredentialSchema = z.object({
14
+ apiKey: z.string().min(1, 'API key is required'),
15
+ name: z.string().optional()
16
+ })
17
+
18
+ export type QwenCredential = z.infer<typeof QwenCredentialSchema>
19
+
20
+ /**
21
+ * Qwen authentication handler
22
+ */
23
+ export const QwenAuthHandler: AuthHandler = {
24
+ // Provider metadata
25
+ provider: {
26
+ id: 'qwen',
27
+ name: 'Qwen',
28
+ description: 'Qwen AI models - high-quality large language models for coding, reasoning, and creative tasks',
29
+ website: 'https://qwen.aikit.club',
30
+ icon: '🤖',
31
+ category: 'ai'
32
+ },
33
+
34
+ // Credential validation
35
+ validateCredential: async (credential: Credential): Promise<boolean> => {
36
+ try {
37
+ const qwenCred = QwenCredentialSchema.parse(credential)
38
+ return await validateQwenApiKey(qwenCred.apiKey)
39
+ } catch (error) {
40
+ console.error('Credential validation error:', error)
41
+ return false
42
+ }
43
+ },
44
+
45
+ // Provider creation
46
+ createProvider: async (credential: Credential) => {
47
+ try {
48
+ const qwenCred = QwenCredentialSchema.parse(credential)
49
+ const baseURL = 'https://qwen.aikit.club/v1'
50
+
51
+ // Validate and fetch models
52
+ const isValid = await validateQwenApiKey(qwenCred.apiKey)
53
+ if (!isValid) {
54
+ throw new Error('Invalid Qwen API key')
55
+ }
56
+
57
+ const models = await fetchQwenModels(qwenCred.apiKey, baseURL)
58
+
59
+ return createQwenProvider({
60
+ apiKey: qwenCred.apiKey,
61
+ baseURL
62
+ })
63
+ } catch (error) {
64
+ throw new Error(`Failed to create Qwen provider: ${error instanceof Error ? error.message : String(error)}`)
65
+ }
66
+ },
67
+
68
+ // Credential templates for user guidance
69
+ credentialTemplates: [
70
+ {
71
+ name: 'API Key',
72
+ fields: [
73
+ {
74
+ key: 'apiKey',
75
+ label: 'API Key',
76
+ type: 'password',
77
+ placeholder: 'sk-...',
78
+ required: true,
79
+ description: 'Your Qwen API key from https://qwen.aikit.club'
80
+ }
81
+ ]
82
+ }
83
+ ],
84
+
85
+ // Additional configuration options
86
+ configOptions: {
87
+ autoRefresh: {
88
+ type: 'boolean',
89
+ default: true,
90
+ description: 'Automatically refresh tokens when they expire'
91
+ },
92
+ timeout: {
93
+ type: 'number',
94
+ default: 30000,
95
+ description: 'Request timeout in milliseconds'
96
+ }
97
+ },
98
+
99
+ // Help and documentation
100
+ help: {
101
+ title: 'Qwen AI Integration',
102
+ description: `
103
+ Qwen provides state-of-the-art language models with dynamic model loading:
104
+
105
+ • **Code Generation**: Qwen Coder, Qwen3-Coder for development tasks
106
+ • **Complex Reasoning**: Qwen Max, Qwen-Deep-Research for advanced problem-solving
107
+ • **Speed & Efficiency**: Qwen Turbo, Qwen-Flash for quick responses
108
+ • **Multimodal**: Qwen VL, Qwen3-Omni for image and text understanding
109
+ • **Web Development**: Qwen-Web-Dev for frontend development
110
+ • **Full-Stack**: Qwen-Full-Stack for complete application development
111
+ • **Video Generation**: Text-to-video capabilities
112
+ • **Image Generation**: Advanced image creation and editing
113
+
114
+ **Getting Started:**
115
+
116
+ 1. Visit [chat.qwen.ai](https://chat.qwen.ai) and log in
117
+ 2. Extract your access token using the browser console script from [qwen-api repo](https://github.com/tanu1337/qwen-api)
118
+ 3. Use \`/connect\` command and select "Qwen"
119
+ 4. Paste your access token when prompted
120
+ 5. Models will be loaded dynamically based on your account access
121
+
122
+ **Dynamic Model Loading:**
123
+ Models are fetched directly from the API, ensuring you always have access to the latest available models. Common models include:
124
+ - \`qwen-turbo\`: Fast, efficient for general tasks
125
+ - \`qwen-plus\`: Balanced performance for complex tasks
126
+ - \`qwen-max\`: Most capable for advanced reasoning
127
+ - \`qwen-coder\`: Specialized for coding and development
128
+ - \`qwen-vl\`: Multimodal, supports images and text
129
+ - \`qwen-deep-research\`: Comprehensive research with web search
130
+ - \`qwen-web-dev\`: Frontend web development
131
+ - \`qwen-full-stack\`: Complete application development
132
+
133
+ **Getting Your Token:**
134
+ 1. Go to [chat.qwen.ai](https://chat.qwen.ai) and login
135
+ 2. Open browser console (F12 → Console tab)
136
+ 3. Paste this JavaScript code:
137
+ \`\`\`javascript
138
+ (function(){if(window.location.hostname!=="chat.qwen.ai"){alert("🚀 This code is for chat.qwen.ai");window.open("https://chat.qwen.ai","_blank");return;}
139
+ function getApiKeyData(){const token=localStorage.getItem("token");if(!token){alert("❌ qwen access_token not found !!!");return null;}
140
+ return token;}
141
+ async function copyToClipboard(text){try{await navigator.clipboard.writeText(text);return true;}catch(err){console.error("❌ Failed to copy to clipboard:",err);const textarea=document.createElement("textarea");textarea.value=text;textarea.style.position="fixed";textarea.style.opacity="0";document.body.appendChild(textarea);textarea.focus();textarea.select();const success=document.execCommand("copy");document.body.removeChild(textarea);return success;}}
142
+ const apiKeyData=getApiKeyData();if(!apiKeyData)return;copyToClipboard(apiKeyData).then((success)=>{if(success){alert("🔑 Qwen access_token copied to clipboard !!! 🎉");}else{prompt("🔰 Qwen access_token:",apiKeyData);}});})();
143
+ \`\`\`
144
+
145
+ For more information, visit the [Qwen API repository](https://github.com/tanu1337/qwen-api).
146
+ `.trim(),
147
+ links: [
148
+ {
149
+ label: 'Get API Key',
150
+ url: 'https://qwen.aikit.club/api-keys'
151
+ },
152
+ {
153
+ label: 'Documentation',
154
+ url: 'https://qwen.aikit.club/docs'
155
+ },
156
+ {
157
+ label: 'Pricing',
158
+ url: 'https://qwen.aikit.club/pricing'
159
+ }
160
+ ]
161
+ }
162
+ }
163
+
164
+ // Export for direct usage
165
+ export { QwenCredentialSchema, type QwenCredential }
package/index.ts ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Qwen Provider for OpenCode
3
+ *
4
+ * This package provides Qwen API integration for OpenCode following the provider specification.
5
+ * It supports both direct API key usage and token refresh capabilities.
6
+ * Models are fetched dynamically from the API.
7
+ */
8
+
9
+ import { createOpenAI } from '@ai-sdk/openai-compatible'
10
+ import type { Provider, Model } from '@ai-sdk/provider'
11
+ import { z } from 'zod'
12
+
13
+ // Qwen API base URL
14
+ const QWEN_BASE_URL = 'https://qwen.aikit.club/v1'
15
+
16
+ // Qwen model interface from API response
17
+ export interface QwenApiModel {
18
+ id: string
19
+ object: string
20
+ created: number
21
+ owned_by: string
22
+ permission?: any[]
23
+ root?: string
24
+ parent?: string
25
+ }
26
+
27
+ export interface QwenModelsResponse {
28
+ object: string
29
+ data: QwenApiModel[]
30
+ }
31
+
32
+ // Model cache to avoid repeated API calls
33
+ let modelCache: Record<string, Model> | null = null
34
+ let modelCacheTime = 0
35
+ const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes
36
+
37
+ // Configuration schema for Qwen provider
38
+ const QwenConfigSchema = z.object({
39
+ apiKey: z.string().min(1, 'API key is required'),
40
+ baseURL: z.string().default(QWEN_BASE_URL),
41
+ refreshToken: z.string().optional(),
42
+ autoRefresh: z.boolean().default(true),
43
+ timeout: z.number().default(30000)
44
+ })
45
+
46
+ export type QwenConfig = z.infer<typeof QwenConfigSchema>
47
+
48
+ /**
49
+ * Convert Qwen API model to OpenCode Model format
50
+ */
51
+ function convertApiModelToModel(apiModel: QwenApiModel): Model {
52
+ const modelId = apiModel.id
53
+
54
+ // Extract capabilities from model name
55
+ const isVisionModel = modelId.includes('vl') || modelId.includes('vision') || modelId.includes('qvq')
56
+ const isCoderModel = modelId.includes('coder') || modelId.includes('code')
57
+ const isTurboModel = modelId.includes('turbo') || modelId.includes('flash')
58
+ const isMaxModel = modelId.includes('max')
59
+ const isPlusModel = modelId.includes('plus')
60
+ const isDeepResearch = modelId.includes('deep-research')
61
+ const isWebDev = modelId.includes('web-dev')
62
+ const isFullStack = modelId.includes('full-stack')
63
+ const isOmni = modelId.includes('omni')
64
+
65
+ // Determine supported capabilities
66
+ const supports: string[] = ['text']
67
+ if (isVisionModel) supports.push('image')
68
+ if (modelId.includes('search') || modelId.includes('plus') || modelId.includes('max')) supports.push('web_search')
69
+ if (isCoderModel || modelId.includes('tools') || modelId.includes('plus') || modelId.includes('max')) supports.push('tools')
70
+ if (modelId.includes('thinking') || modelId.includes('max')) supports.push('thinking')
71
+
72
+ // Determine max tokens based on model
73
+ let maxTokens = 8192
74
+ if (modelId.includes('32b') || modelId.includes('80b') || modelId.includes('235b') ||
75
+ modelId.includes('max') || modelId.includes('plus') || isCoderModel) {
76
+ maxTokens = 32768
77
+ } else if (modelId.includes('14b')) {
78
+ maxTokens = 8192
79
+ } else if (modelId.includes('72b')) {
80
+ maxTokens = 32768
81
+ }
82
+
83
+ // Generate description
84
+ let description = ''
85
+ if (isTurboModel) description = 'Fast and efficient model for general tasks'
86
+ else if (isMaxModel) description = 'Most capable model for advanced tasks'
87
+ else if (isPlusModel) description = 'Balanced model for complex reasoning'
88
+ else if (isCoderModel) description = 'Specialized model for coding and development'
89
+ else if (isDeepResearch) description = 'Research model with comprehensive analysis capabilities'
90
+ else if (isWebDev) description = 'Specialized for web development and UI generation'
91
+ else if (isFullStack) description = 'Full-stack application development model'
92
+ else if (isVisionModel) description = 'Multimodal model supporting text and image analysis'
93
+ else description = 'Qwen language model'
94
+
95
+ return {
96
+ name: apiModel.id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
97
+ provider: 'Qwen',
98
+ maxTokens,
99
+ supports,
100
+ description
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Fetch models from Qwen API
106
+ */
107
+ export async function fetchQwenModels(apiKey: string, baseURL: string = QWEN_BASE_URL): Promise<Record<string, Model>> {
108
+ const now = Date.now()
109
+
110
+ // Return cached models if still valid
111
+ if (modelCache && (now - modelCacheTime) < CACHE_DURATION) {
112
+ return modelCache
113
+ }
114
+
115
+ try {
116
+ const response = await fetch(`${baseURL}/models`, {
117
+ method: 'GET',
118
+ headers: {
119
+ 'Authorization': `Bearer ${apiKey}`,
120
+ 'Content-Type': 'application/json',
121
+ 'User-Agent': 'OpenCode-Qwen-Provider/1.0.0'
122
+ }
123
+ })
124
+
125
+ if (!response.ok) {
126
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)
127
+ }
128
+
129
+ const data: QwenModelsResponse = await response.json()
130
+
131
+ // Convert API models to OpenCode format
132
+ const models: Record<string, Model> = {}
133
+ for (const apiModel of data.data) {
134
+ models[apiModel.id] = convertApiModelToModel(apiModel)
135
+ }
136
+
137
+ // Cache the results
138
+ modelCache = models
139
+ modelCacheTime = now
140
+
141
+ return models
142
+ } catch (error) {
143
+ console.error('Failed to fetch Qwen models:', error)
144
+
145
+ // Return fallback models if cache exists but is expired
146
+ if (modelCache) {
147
+ console.warn('Using cached models due to API error')
148
+ return modelCache
149
+ }
150
+
151
+ // Return minimal fallback models
152
+ return {
153
+ 'qwen-turbo': {
154
+ name: 'Qwen Turbo',
155
+ provider: 'Qwen',
156
+ maxTokens: 8192,
157
+ supports: ['text'],
158
+ description: 'Fast and efficient model for general tasks'
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Create Qwen provider instance
166
+ */
167
+ export function createQwenProvider(config: QwenConfig): Provider {
168
+ const validatedConfig = QwenConfigSchema.parse(config)
169
+
170
+ return createOpenAI({
171
+ name: 'Qwen',
172
+ baseURL: validatedConfig.baseURL,
173
+ apiKey: validatedConfig.apiKey,
174
+ headers: {
175
+ 'User-Agent': 'OpenCode-Qwen-Provider/1.0.0'
176
+ },
177
+ compatibility: 'compatible'
178
+ })
179
+ }
180
+
181
+ /**
182
+ * Validate Qwen API key
183
+ */
184
+ export async function validateQwenApiKey(apiKey: string): Promise<boolean> {
185
+ try {
186
+ const response = await fetch(`${QWEN_BASE_URL}/models`, {
187
+ method: 'GET',
188
+ headers: {
189
+ 'Authorization': `Bearer ${apiKey}`,
190
+ 'Content-Type': 'application/json'
191
+ }
192
+ })
193
+
194
+ return response.ok
195
+ } catch {
196
+ return false
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get available Qwen models (requires API key for dynamic loading)
202
+ */
203
+ export async function getQwenModels(apiKey: string, baseURL?: string): Promise<Record<string, Model>> {
204
+ return await fetchQwenModels(apiKey, baseURL)
205
+ }
206
+
207
+ /**
208
+ * Provider factory for OpenCode integration
209
+ */
210
+ export default function qwenProvider() {
211
+ return {
212
+ id: 'qwen',
213
+ name: 'Qwen',
214
+ description: 'Qwen AI models - high-quality large language models with dynamic model loading',
215
+ website: 'https://qwen.aikit.club',
216
+ create: createQwenProvider,
217
+ validate: validateQwenApiKey,
218
+ getModels: fetchQwenModels
219
+ }
220
+ }
221
+
222
+ // Export types for TypeScript users
223
+ export type { QwenConfig }
@@ -0,0 +1,34 @@
1
+ # Example OpenCode configuration with Qwen auth plugin
2
+
3
+ {
4
+ "$schema": "https://opencode.ai/config.json",
5
+ "plugins": [
6
+ "./plugins/qwen-auth.ts"
7
+ ],
8
+ "tools": {
9
+ "qwen": {
10
+ "baseUrl": "https://qwen.aikit.club/v1",
11
+ "apiKey": "${QWEN_API_KEY}"
12
+ }
13
+ },
14
+ "env": {
15
+ "QWEN_API_KEY": {
16
+ "description": "Qwen API access token",
17
+ "required": true
18
+ },
19
+ "QWEN_REFRESH_TOKEN": {
20
+ "description": "Qwen API refresh token for automatic renewal",
21
+ "required": false
22
+ },
23
+ "QWEN_AUTO_REFRESH": {
24
+ "description": "Enable automatic token refresh",
25
+ "default": "true",
26
+ "type": "boolean"
27
+ },
28
+ "QWEN_VALIDATE_ON_STARTUP": {
29
+ "description": "Validate token on session startup",
30
+ "default": "true",
31
+ "type": "boolean"
32
+ }
33
+ }
34
+ }
package/opencode.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "provider": {
4
+ "qwen": {
5
+ "npm": "opencode-qwen",
6
+ "name": "Qwen AI",
7
+ "description": "High-quality large language models with dynamic model loading - optimized for coding, reasoning, web development, and creative tasks",
8
+ "website": "https://qwen.aikit.club",
9
+ "documentation": "https://github.com/tanu1337/qwen-api",
10
+ "options": {
11
+ "baseURL": "https://qwen.aikit.club/v1",
12
+ "dynamicModels": true,
13
+ "modelCacheDuration": 300000
14
+ },
15
+ "authentication": {
16
+ "type": "bearer",
17
+ "help": "Get your access token from https://chat.qwen.ai by running the token extractor script in browser console"
18
+ }
19
+ }
20
+ },
21
+ "env": {
22
+ "QWEN_API_KEY": {
23
+ "description": "Qwen API access key",
24
+ "required": true
25
+ },
26
+ "QWEN_BASE_URL": {
27
+ "description": "Qwen API base URL",
28
+ "default": "https://qwen.aikit.club/v1"
29
+ },
30
+ "QWEN_AUTO_REFRESH": {
31
+ "description": "Enable automatic token refresh",
32
+ "default": "true",
33
+ "type": "boolean"
34
+ },
35
+ "QWEN_TIMEOUT": {
36
+ "description": "Request timeout in milliseconds",
37
+ "default": "30000",
38
+ "type": "number"
39
+ }
40
+ }
41
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "opencode-qwen",
3
+ "version": "1.0.0",
4
+ "description": "Qwen provider for OpenCode with npm package support",
5
+ "main": "index.ts",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "dependencies": {
10
+ "@opencode-ai/plugin": "^1.0.0",
11
+ "zod": "^3.22.0",
12
+ "@ai-sdk/openai-compatible": "^1.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.0.0",
16
+ "@types/node": "^20.0.0"
17
+ },
18
+ "keywords": [
19
+ "opencode",
20
+ "plugin",
21
+ "provider",
22
+ "qwen",
23
+ "ai"
24
+ ],
25
+ "author": "OpenCode Community",
26
+ "license": "MIT"
27
+ }
package/qwen-auth.ts ADDED
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Qwen Provider Authentication Plugin for OpenCode
3
+ *
4
+ * This plugin handles authentication for the Qwen API provider,
5
+ * including token validation, refresh, and secure credential management.
6
+ */
7
+
8
+ import type { Plugin, tool } from "@opencode-ai/plugin"
9
+ import { z } from "zod"
10
+
11
+ // Qwen API base URL
12
+ const QWEN_API_BASE = "https://qwen.aikit.club/v1"
13
+
14
+ // TypeScript types for Qwen authentication
15
+ export interface QwenAuthState {
16
+ accessToken?: string
17
+ refreshToken?: string
18
+ expiresAt?: number
19
+ isValid: boolean
20
+ lastValidated?: number
21
+ }
22
+
23
+ export interface QwenTokenResponse {
24
+ access_token: string
25
+ refresh_token?: string
26
+ expires_in: number
27
+ token_type: string
28
+ }
29
+
30
+ export interface QwenModel {
31
+ id: string
32
+ object: string
33
+ created: number
34
+ owned_by: string
35
+ }
36
+
37
+ export interface QwenModelsResponse {
38
+ object: string
39
+ data: QwenModel[]
40
+ }
41
+
42
+ // Zod schemas for validation
43
+ const QwenAuthConfigSchema = z.object({
44
+ apiKey: z.string().min(1, "API key is required"),
45
+ refreshToken: z.string().optional(),
46
+ autoRefresh: z.boolean().default(true),
47
+ validateOnStartup: z.boolean().default(true)
48
+ })
49
+
50
+ const ValidateTokenSchema = z.object({
51
+ token: z.string().optional()
52
+ })
53
+
54
+ const RefreshTokenSchema = z.object({
55
+ refreshToken: z.string().optional()
56
+ })
57
+
58
+ /**
59
+ * Qwen Authentication Plugin
60
+ */
61
+ export const QwenAuthPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
62
+ let authState: QwenAuthState = {
63
+ isValid: false
64
+ }
65
+
66
+ /**
67
+ * Load authentication state from environment or secure storage
68
+ */
69
+ const loadAuthState = async (): Promise<QwenAuthState> => {
70
+ try {
71
+ const config = QwenAuthConfigSchema.parse({
72
+ apiKey: process.env.QWEN_API_KEY,
73
+ refreshToken: process.env.QWEN_REFRESH_TOKEN,
74
+ autoRefresh: process.env.QWEN_AUTO_REFRESH !== 'false',
75
+ validateOnStartup: process.env.QWEN_VALIDATE_ON_STARTUP !== 'false'
76
+ })
77
+
78
+ authState = {
79
+ accessToken: config.apiKey,
80
+ refreshToken: config.refreshToken,
81
+ isValid: !!config.apiKey,
82
+ lastValidated: Date.now()
83
+ }
84
+
85
+ await client.app.log({
86
+ service: "qwen-auth",
87
+ level: "info",
88
+ message: "Auth state loaded",
89
+ extra: { hasToken: !!config.apiKey, hasRefreshToken: !!config.refreshToken }
90
+ })
91
+
92
+ return authState
93
+ } catch (error) {
94
+ await client.app.log({
95
+ service: "qwen-auth",
96
+ level: "error",
97
+ message: "Failed to load auth state",
98
+ extra: { error: error instanceof Error ? error.message : String(error) }
99
+ })
100
+ return { isValid: false }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Validate current access token
106
+ */
107
+ const validateToken = async (token?: string): Promise<boolean> => {
108
+ try {
109
+ const tokenToValidate = token || authState.accessToken
110
+ if (!tokenToValidate) {
111
+ return false
112
+ }
113
+
114
+ const response = await fetch(`${QWEN_API_BASE}/validate`, {
115
+ method: "GET",
116
+ headers: {
117
+ "Authorization": `Bearer ${tokenToValidate}`,
118
+ "Content-Type": "application/json"
119
+ }
120
+ })
121
+
122
+ const isValid = response.ok
123
+ authState.isValid = isValid
124
+ authState.lastValidated = Date.now()
125
+
126
+ await client.app.log({
127
+ service: "qwen-auth",
128
+ level: isValid ? "info" : "warn",
129
+ message: `Token validation ${isValid ? 'successful' : 'failed'}`,
130
+ extra: { status: response.status }
131
+ })
132
+
133
+ return isValid
134
+ } catch (error) {
135
+ authState.isValid = false
136
+ await client.app.log({
137
+ service: "qwen-auth",
138
+ level: "error",
139
+ message: "Token validation error",
140
+ extra: { error: error instanceof Error ? error.message : String(error) }
141
+ })
142
+ return false
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Refresh access token using refresh token
148
+ */
149
+ const refreshToken = async (refreshTokenValue?: string): Promise<boolean> => {
150
+ try {
151
+ const tokenToUse = refreshTokenValue || authState.refreshToken
152
+ if (!tokenToUse) {
153
+ await client.app.log({
154
+ service: "qwen-auth",
155
+ level: "warn",
156
+ message: "No refresh token available"
157
+ })
158
+ return false
159
+ }
160
+
161
+ const response = await fetch(`${QWEN_API_BASE}/refresh`, {
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json"
165
+ },
166
+ body: JSON.stringify({
167
+ refresh_token: tokenToUse
168
+ })
169
+ })
170
+
171
+ if (!response.ok) {
172
+ throw new Error(`Refresh failed: ${response.status}`)
173
+ }
174
+
175
+ const tokenData: QwenTokenResponse = await response.json()
176
+
177
+ authState.accessToken = tokenData.access_token
178
+ if (tokenData.refresh_token) {
179
+ authState.refreshToken = tokenData.refresh_token
180
+ }
181
+ authState.expiresAt = Date.now() + (tokenData.expires_in * 1000)
182
+ authState.isValid = true
183
+ authState.lastValidated = Date.now()
184
+
185
+ await client.app.log({
186
+ service: "qwen-auth",
187
+ level: "info",
188
+ message: "Token refreshed successfully",
189
+ extra: { expiresIn: tokenData.expires_in }
190
+ })
191
+
192
+ return true
193
+ } catch (error) {
194
+ await client.app.log({
195
+ service: "qwen-auth",
196
+ level: "error",
197
+ message: "Token refresh failed",
198
+ extra: { error: error instanceof Error ? error.message : String(error) }
199
+ })
200
+ return false
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Auto-refresh token if needed
206
+ */
207
+ const ensureValidToken = async (): Promise<boolean> => {
208
+ if (!authState.accessToken) {
209
+ await loadAuthState()
210
+ }
211
+
212
+ if (authState.isValid && authState.lastValidated &&
213
+ Date.now() - authState.lastValidated < 5 * 60 * 1000) {
214
+ return true // Valid within last 5 minutes
215
+ }
216
+
217
+ const isValid = await validateToken()
218
+ if (isValid) {
219
+ return true
220
+ }
221
+
222
+ if (authState.refreshToken) {
223
+ return await refreshToken()
224
+ }
225
+
226
+ return false
227
+ }
228
+
229
+ // Initialize plugin
230
+ await loadAuthState()
231
+
232
+ return {
233
+ // Custom tools for Qwen authentication
234
+ tool: {
235
+ "qwen.validate-token": tool({
236
+ description: "Validate Qwen API access token",
237
+ args: ValidateTokenSchema,
238
+ async execute(args, context) {
239
+ const isValid = await validateToken(args.token)
240
+ return {
241
+ valid: isValid,
242
+ timestamp: new Date().toISOString(),
243
+ state: authState
244
+ }
245
+ }
246
+ }),
247
+
248
+ "qwen.refresh-token": tool({
249
+ description: "Refresh Qwen API access token",
250
+ args: RefreshTokenSchema,
251
+ async execute(args, context) {
252
+ const success = await refreshToken(args.refreshToken)
253
+ return {
254
+ success,
255
+ timestamp: new Date().toISOString(),
256
+ state: authState
257
+ }
258
+ }
259
+ }),
260
+
261
+ "qwen.list-models": tool({
262
+ description: "List available Qwen models (requires valid authentication)",
263
+ args: z.object({}),
264
+ async execute(args, context) {
265
+ await ensureValidToken()
266
+
267
+ if (!authState.accessToken || !authState.isValid) {
268
+ throw new Error("No valid Qwen authentication token available")
269
+ }
270
+
271
+ try {
272
+ const response = await fetch(`${QWEN_API_BASE}/models`, {
273
+ headers: {
274
+ "Authorization": `Bearer ${authState.accessToken}`,
275
+ "Content-Type": "application/json"
276
+ }
277
+ })
278
+
279
+ if (!response.ok) {
280
+ throw new Error(`Failed to fetch models: ${response.status}`)
281
+ }
282
+
283
+ const models: QwenModelsResponse = await response.json()
284
+
285
+ await client.app.log({
286
+ service: "qwen-auth",
287
+ level: "info",
288
+ message: `Successfully retrieved ${models.data.length} models`
289
+ })
290
+
291
+ return models
292
+ } catch (error) {
293
+ await client.app.log({
294
+ service: "qwen-auth",
295
+ level: "error",
296
+ message: "Failed to fetch models",
297
+ extra: { error: error instanceof Error ? error.message : String(error) }
298
+ })
299
+ throw error
300
+ }
301
+ }
302
+ })
303
+ },
304
+
305
+ // Hooks for automatic token management
306
+ "tool.execute.before": async (input, output) => {
307
+ // Intercept Qwen API calls and ensure valid authentication
308
+ if (input.tool === "bash" && output.args.command?.includes("qwen.aikit.club")) {
309
+ await ensureValidToken()
310
+ if (authState.accessToken) {
311
+ // Inject token into command if needed
312
+ output.args.command = output.args.command.replace(
313
+ /qwen\.aikit\.club\/v1\//,
314
+ `qwen.aikit.club/v1/?api_key=${authState.accessToken}&`
315
+ )
316
+ }
317
+ }
318
+ },
319
+
320
+ "session.created": async () => {
321
+ // Validate token on session creation if enabled
322
+ if (process.env.QWEN_VALIDATE_ON_STARTUP !== 'false') {
323
+ await ensureValidToken()
324
+ }
325
+ },
326
+
327
+ "session.idle": async () => {
328
+ // Periodic validation when session is idle
329
+ if (authState.isValid && authState.lastValidated) {
330
+ const timeSinceValidation = Date.now() - authState.lastValidated
331
+ if (timeSinceValidation > 30 * 60 * 1000) { // 30 minutes
332
+ await validateToken()
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }