opencode-qwen 1.0.2 → 1.0.5

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/fix_summary.md DELETED
@@ -1,28 +0,0 @@
1
- ## Qwen Plugin Import Fix Summary
2
-
3
- **Problem Identified:**
4
- The plugin was failing to load because it was trying to import `createOpenAI` from the non-existent package `@ai-sdk/openai-compatible`, but this function actually exists in `@ai-sdk/openai`.
5
-
6
- **Root Cause:**
7
- - Package `@ai-sdk/openai-compatible` doesn't exist in the OpenCode environment
8
- - Function `createOpenAI` is available in `@ai-sdk/openai` package
9
- - Multiple files had incorrect import statements
10
-
11
- **Fixes Applied:**
12
- 1. Fixed import in `/root/workspace/opencode-qwen/index.ts`:
13
- - Changed: `import { createOpenAI } from '@ai-sdk/openai-compatible'`
14
- - To: `import { createOpenAI } from '@ai-sdk/openai'`
15
-
16
- 2. Fixed dependency in `/root/workspace/opencode-qwen/package.json`:
17
- - Changed: `"@ai-sdk/openai-compatible": "^1.0.0"`
18
- - To: `"@ai-sdk/openai": "^1.0.0"` (will need version update)
19
-
20
- **Next Steps:**
21
- - Update package.json dependency version to match available @ai-sdk/openai version
22
- - Restart OpenCode to test the fix
23
- - Monitor logs to ensure plugin loads successfully
24
-
25
- **Technical Details:**
26
- - Available @ai-sdk/openai version: 3.0.23
27
- - The createOpenAI function is confirmed to exist in @ai-sdk/openai/dist/index.mjs
28
- - Plugin should now be able to load without import errors
package/index.ts DELETED
@@ -1,222 +0,0 @@
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'
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
-
169
- return createOpenAI({
170
- name: 'Qwen',
171
- baseURL: config.baseURL || QWEN_BASE_URL,
172
- apiKey: config.apiKey,
173
- headers: {
174
- 'User-Agent': 'OpenCode-Qwen-Provider/1.0.0'
175
- },
176
- compatibility: 'compatible'
177
- })
178
- }
179
-
180
- /**
181
- * Validate Qwen API key
182
- */
183
- export async function validateQwenApiKey(apiKey: string): Promise<boolean> {
184
- try {
185
- const response = await fetch(`${QWEN_BASE_URL}/models`, {
186
- method: 'GET',
187
- headers: {
188
- 'Authorization': `Bearer ${apiKey}`,
189
- 'Content-Type': 'application/json'
190
- }
191
- })
192
-
193
- return response.ok
194
- } catch {
195
- return false
196
- }
197
- }
198
-
199
- /**
200
- * Get available Qwen models (requires API key for dynamic loading)
201
- */
202
- export async function getQwenModels(apiKey: string, baseURL?: string): Promise<Record<string, Model>> {
203
- return await fetchQwenModels(apiKey, baseURL)
204
- }
205
-
206
- /**
207
- * Provider factory for OpenCode integration
208
- */
209
- export default function qwenProvider() {
210
- return {
211
- id: 'qwen',
212
- name: 'Qwen',
213
- description: 'Qwen AI models - high-quality large language models with dynamic model loading',
214
- website: 'https://qwen.aikit.club',
215
- create: createQwenProvider,
216
- validate: validateQwenApiKey,
217
- getModels: fetchQwenModels
218
- }
219
- }
220
-
221
- // Export types for TypeScript users
222
- export type { QwenConfig }
@@ -1,34 +0,0 @@
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/qwen-auth.ts DELETED
@@ -1,353 +0,0 @@
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
- // 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
- }
93
-
94
- authState = {
95
- accessToken: config.apiKey,
96
- refreshToken: config.refreshToken,
97
- isValid: !!config.apiKey,
98
- lastValidated: Date.now()
99
- }
100
-
101
- await client.app.log({
102
- service: "qwen-auth",
103
- level: "info",
104
- message: "Auth state loaded",
105
- extra: { hasToken: !!config.apiKey, hasRefreshToken: !!config.refreshToken }
106
- })
107
-
108
- return authState
109
- } catch (error) {
110
- await client.app.log({
111
- service: "qwen-auth",
112
- level: "error",
113
- message: "Failed to load auth state",
114
- extra: { error: error instanceof Error ? error.message : String(error) }
115
- })
116
- return { isValid: false }
117
- }
118
- }
119
-
120
- /**
121
- * Validate current access token
122
- */
123
- const validateToken = async (token?: string): Promise<boolean> => {
124
- try {
125
- const tokenToValidate = token || authState.accessToken
126
- if (!tokenToValidate) {
127
- return false
128
- }
129
-
130
- const response = await fetch(`${QWEN_API_BASE}/validate`, {
131
- method: "GET",
132
- headers: {
133
- "Authorization": `Bearer ${tokenToValidate}`,
134
- "Content-Type": "application/json"
135
- }
136
- })
137
-
138
- const isValid = response.ok
139
- authState.isValid = isValid
140
- authState.lastValidated = Date.now()
141
-
142
- await client.app.log({
143
- service: "qwen-auth",
144
- level: isValid ? "info" : "warn",
145
- message: `Token validation ${isValid ? 'successful' : 'failed'}`,
146
- extra: { status: response.status }
147
- })
148
-
149
- return isValid
150
- } catch (error) {
151
- authState.isValid = false
152
- await client.app.log({
153
- service: "qwen-auth",
154
- level: "error",
155
- message: "Token validation error",
156
- extra: { error: error instanceof Error ? error.message : String(error) }
157
- })
158
- return false
159
- }
160
- }
161
-
162
- /**
163
- * Refresh access token using refresh token
164
- */
165
- const refreshToken = async (refreshTokenValue?: string): Promise<boolean> => {
166
- try {
167
- const tokenToUse = refreshTokenValue || authState.refreshToken
168
- if (!tokenToUse) {
169
- await client.app.log({
170
- service: "qwen-auth",
171
- level: "warn",
172
- message: "No refresh token available"
173
- })
174
- return false
175
- }
176
-
177
- const response = await fetch(`${QWEN_API_BASE}/refresh`, {
178
- method: "POST",
179
- headers: {
180
- "Content-Type": "application/json"
181
- },
182
- body: JSON.stringify({
183
- refresh_token: tokenToUse
184
- })
185
- })
186
-
187
- if (!response.ok) {
188
- throw new Error(`Refresh failed: ${response.status}`)
189
- }
190
-
191
- const tokenData: QwenTokenResponse = await response.json()
192
-
193
- authState.accessToken = tokenData.access_token
194
- if (tokenData.refresh_token) {
195
- authState.refreshToken = tokenData.refresh_token
196
- }
197
- authState.expiresAt = Date.now() + (tokenData.expires_in * 1000)
198
- authState.isValid = true
199
- authState.lastValidated = Date.now()
200
-
201
- await client.app.log({
202
- service: "qwen-auth",
203
- level: "info",
204
- message: "Token refreshed successfully",
205
- extra: { expiresIn: tokenData.expires_in }
206
- })
207
-
208
- return true
209
- } catch (error) {
210
- await client.app.log({
211
- service: "qwen-auth",
212
- level: "error",
213
- message: "Token refresh failed",
214
- extra: { error: error instanceof Error ? error.message : String(error) }
215
- })
216
- return false
217
- }
218
- }
219
-
220
- /**
221
- * Auto-refresh token if needed
222
- */
223
- const ensureValidToken = async (): Promise<boolean> => {
224
- if (!authState.accessToken) {
225
- await loadAuthState()
226
- }
227
-
228
- if (authState.isValid && authState.lastValidated &&
229
- Date.now() - authState.lastValidated < 5 * 60 * 1000) {
230
- return true // Valid within last 5 minutes
231
- }
232
-
233
- const isValid = await validateToken()
234
- if (isValid) {
235
- return true
236
- }
237
-
238
- if (authState.refreshToken) {
239
- return await refreshToken()
240
- }
241
-
242
- return false
243
- }
244
-
245
- // Initialize plugin
246
- await loadAuthState()
247
-
248
- return {
249
- // Custom tools for Qwen authentication
250
- tool: {
251
- "qwen.validate-token": tool({
252
- description: "Validate Qwen API access token",
253
- args: ValidateTokenSchema,
254
- async execute(args, context) {
255
- const isValid = await validateToken(args.token)
256
- return {
257
- valid: isValid,
258
- timestamp: new Date().toISOString(),
259
- state: authState
260
- }
261
- }
262
- }),
263
-
264
- "qwen.refresh-token": tool({
265
- description: "Refresh Qwen API access token",
266
- args: RefreshTokenSchema,
267
- async execute(args, context) {
268
- const success = await refreshToken(args.refreshToken)
269
- return {
270
- success,
271
- timestamp: new Date().toISOString(),
272
- state: authState
273
- }
274
- }
275
- }),
276
-
277
- "qwen.list-models": tool({
278
- description: "List available Qwen models (requires valid authentication)",
279
- args: z.object({}),
280
- async execute(args, context) {
281
- await ensureValidToken()
282
-
283
- if (!authState.accessToken || !authState.isValid) {
284
- throw new Error("No valid Qwen authentication token available")
285
- }
286
-
287
- try {
288
- const response = await fetch(`${QWEN_API_BASE}/models`, {
289
- headers: {
290
- "Authorization": `Bearer ${authState.accessToken}`,
291
- "Content-Type": "application/json"
292
- }
293
- })
294
-
295
- if (!response.ok) {
296
- throw new Error(`Failed to fetch models: ${response.status}`)
297
- }
298
-
299
- const models: QwenModelsResponse = await response.json()
300
-
301
- await client.app.log({
302
- service: "qwen-auth",
303
- level: "info",
304
- message: `Successfully retrieved ${models.data.length} models`
305
- })
306
-
307
- return models
308
- } catch (error) {
309
- await client.app.log({
310
- service: "qwen-auth",
311
- level: "error",
312
- message: "Failed to fetch models",
313
- extra: { error: error instanceof Error ? error.message : String(error) }
314
- })
315
- throw error
316
- }
317
- }
318
- })
319
- },
320
-
321
- // Hooks for automatic token management
322
- "tool.execute.before": async (input, output) => {
323
- // Intercept Qwen API calls and ensure valid authentication
324
- if (input.tool === "bash" && output.args.command?.includes("qwen.aikit.club")) {
325
- await ensureValidToken()
326
- if (authState.accessToken) {
327
- // Inject token into command if needed
328
- output.args.command = output.args.command.replace(
329
- /qwen\.aikit\.club\/v1\//,
330
- `qwen.aikit.club/v1/?api_key=${authState.accessToken}&`
331
- )
332
- }
333
- }
334
- },
335
-
336
- "session.created": async () => {
337
- // Validate token on session creation if enabled
338
- if (process.env.QWEN_VALIDATE_ON_STARTUP !== 'false') {
339
- await ensureValidToken()
340
- }
341
- },
342
-
343
- "session.idle": async () => {
344
- // Periodic validation when session is idle
345
- if (authState.isValid && authState.lastValidated) {
346
- const timeSinceValidation = Date.now() - authState.lastValidated
347
- if (timeSinceValidation > 30 * 60 * 1000) { // 30 minutes
348
- await validateToken()
349
- }
350
- }
351
- }
352
- }
353
- }