opencode-qwen 1.0.1 → 1.0.2
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/index.ts +2 -3
- package/package.json +1 -1
- package/qwen-auth.ts +22 -6
- package/qwen-auth.ts.backup +337 -0
- package/test_fix.md +69 -0
package/index.ts
CHANGED
|
@@ -165,12 +165,11 @@ export async function fetchQwenModels(apiKey: string, baseURL: string = QWEN_BAS
|
|
|
165
165
|
* Create Qwen provider instance
|
|
166
166
|
*/
|
|
167
167
|
export function createQwenProvider(config: QwenConfig): Provider {
|
|
168
|
-
const validatedConfig = QwenConfigSchema.parse(config)
|
|
169
168
|
|
|
170
169
|
return createOpenAI({
|
|
171
170
|
name: 'Qwen',
|
|
172
|
-
baseURL:
|
|
173
|
-
apiKey:
|
|
171
|
+
baseURL: config.baseURL || QWEN_BASE_URL,
|
|
172
|
+
apiKey: config.apiKey,
|
|
174
173
|
headers: {
|
|
175
174
|
'User-Agent': 'OpenCode-Qwen-Provider/1.0.0'
|
|
176
175
|
},
|
package/package.json
CHANGED
package/qwen-auth.ts
CHANGED
|
@@ -68,12 +68,28 @@ export const QwenAuthPlugin: Plugin = async ({ project, client, $, directory, wo
|
|
|
68
68
|
*/
|
|
69
69
|
const loadAuthState = async (): Promise<QwenAuthState> => {
|
|
70
70
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// Handle missing environment variables gracefully
|
|
72
|
+
const apiKey = process.env.QWEN_API_KEY
|
|
73
|
+
const refreshToken = process.env.QWEN_REFRESH_TOKEN
|
|
74
|
+
const autoRefresh = process.env.QWEN_AUTO_REFRESH !== "false"
|
|
75
|
+
const validateOnStartup = process.env.QWEN_VALIDATE_ON_STARTUP !== "false"
|
|
76
|
+
|
|
77
|
+
// Only validate if we have required fields
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
await client.app.log({
|
|
80
|
+
service: "qwen-auth",
|
|
81
|
+
level: "info",
|
|
82
|
+
message: "No Qwen API key found in environment"
|
|
83
|
+
})
|
|
84
|
+
return { isValid: false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = {
|
|
88
|
+
apiKey,
|
|
89
|
+
refreshToken,
|
|
90
|
+
autoRefresh,
|
|
91
|
+
validateOnStartup
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
authState = {
|
|
79
95
|
accessToken: config.apiKey,
|
|
@@ -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
|
+
}
|
package/test_fix.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Qwen Plugin Fix Summary
|
|
2
|
+
|
|
3
|
+
## Issues Fixed
|
|
4
|
+
|
|
5
|
+
### 1. Import Error (RESOLVED)
|
|
6
|
+
- **Problem**: `createOpenAI` imported from non-existent `@ai-sdk/openai-compatible`
|
|
7
|
+
- **Solution**: Changed import to `@ai-sdk/openai` package
|
|
8
|
+
- **Files Modified**: `index.ts`, `package.json`
|
|
9
|
+
|
|
10
|
+
### 2. API Key Validation Error (RESOLVED)
|
|
11
|
+
- **Problem**: Zod schema validation on startup with undefined apiKey
|
|
12
|
+
- **Root Cause**: Plugin tried to validate config before user connects
|
|
13
|
+
- **Solution**: Removed immediate validation, added graceful handling
|
|
14
|
+
|
|
15
|
+
## Changes Made
|
|
16
|
+
|
|
17
|
+
### index.ts
|
|
18
|
+
```typescript
|
|
19
|
+
// BEFORE (caused startup error)
|
|
20
|
+
export function createQwenProvider(config: QwenConfig): Provider {
|
|
21
|
+
const validatedConfig = QwenConfigSchema.parse(config) // ← This line failed
|
|
22
|
+
return createOpenAI({
|
|
23
|
+
baseURL: validatedConfig.baseURL,
|
|
24
|
+
apiKey: validatedConfig.apiKey,
|
|
25
|
+
// ...
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// AFTER (graceful handling)
|
|
30
|
+
export function createQwenProvider(config: QwenConfig): Provider {
|
|
31
|
+
return createOpenAI({
|
|
32
|
+
baseURL: config.baseURL || QWEN_BASE_URL, // ← Fallback added
|
|
33
|
+
apiKey: config.apiKey, // ← Direct access
|
|
34
|
+
// ...
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### qwen-auth.ts
|
|
40
|
+
```typescript
|
|
41
|
+
// BEFORE (caused startup error)
|
|
42
|
+
const config = QwenAuthConfigSchema.parse({
|
|
43
|
+
apiKey: process.env.QWEN_API_KEY, // ← Failed if undefined
|
|
44
|
+
// ...
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// AFTER (graceful handling)
|
|
48
|
+
const apiKey = process.env.QWEN_API_KEY
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
await client.app.log({
|
|
51
|
+
service: "qwen-auth",
|
|
52
|
+
level: "info",
|
|
53
|
+
message: "No Qwen API key found in environment"
|
|
54
|
+
})
|
|
55
|
+
return { isValid: false }
|
|
56
|
+
}
|
|
57
|
+
const config = { apiKey, refreshToken, autoRefresh, validateOnStartup }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Expected Behavior
|
|
61
|
+
- Plugin loads without errors on OpenCode startup
|
|
62
|
+
- API key validation only happens when user actually connects
|
|
63
|
+
- Graceful fallback when no credentials are available
|
|
64
|
+
- Proper error handling and logging throughout
|
|
65
|
+
|
|
66
|
+
## Next Steps
|
|
67
|
+
1. Test plugin load (should be error-free now)
|
|
68
|
+
2. Test user connection flow with valid API key
|
|
69
|
+
3. Verify model loading and functionality
|