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.
- package/.github/workflows/main.yml +30 -0
- package/README.md +142 -0
- package/auth.ts +165 -0
- package/index.ts +223 -0
- package/opencode.example.json +34 -0
- package/opencode.json +41 -0
- package/package.json +27 -0
- package/qwen-auth.ts +337 -0
|
@@ -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
|
+
}
|