99 */
1010
1111import { spawn } from 'node:child_process' ;
12+ import { randomUUID } from 'node:crypto' ;
1213
1314import { QWEN_PROVIDER_ID , QWEN_API_CONFIG , QWEN_MODELS , QWEN_OFFICIAL_HEADERS } from './constants.js' ;
1415import type { QwenCredentials } from './types.js' ;
15- import type { HttpError } from './utils/retry.js' ;
16- import { saveCredentials , loadCredentials , resolveBaseUrl } from './plugin/auth.js' ;
16+ import { resolveBaseUrl } from './plugin/auth.js' ;
1717import {
1818 generatePKCE ,
1919 requestDeviceAuthorization ,
2020 pollDeviceToken ,
2121 tokenResponseToCredentials ,
22- refreshAccessToken ,
2322 SlowDownError ,
2423} from './qwen/oauth.js' ;
25- import { logTechnicalDetail } from './errors.js' ;
26- import { retryWithBackoff } from './utils/retry.js' ;
24+ import { retryWithBackoff , getErrorStatus } from './utils/retry.js' ;
2725import { RequestQueue } from './plugin/request-queue.js' ;
26+ import { tokenManager } from './plugin/token-manager.js' ;
27+ import { createDebugLogger } from './utils/debug-logger.js' ;
28+
29+ const debugLogger = createDebugLogger ( 'PLUGIN' ) ;
2830
2931// Global session ID for the plugin lifetime
30- const PLUGIN_SESSION_ID = crypto . randomUUID ( ) ;
32+ const PLUGIN_SESSION_ID = randomUUID ( ) ;
3133
3234// Singleton request queue for throttling (shared across all requests)
3335const requestQueue = new RequestQueue ( ) ;
@@ -86,7 +88,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
8688 provider : QWEN_PROVIDER_ID ,
8789
8890 loader : async (
89- getAuth : ( ) => Promise < { type : string ; access ?: string ; refresh ?: string ; expires ?: number } > ,
91+ getAuth : any ,
9092 provider : { models ?: Record < string , { cost ?: { input : number ; output : number } } > } ,
9193 ) => {
9294 // Zerar custo dos modelos (gratuito via OAuth)
@@ -96,66 +98,103 @@ export const QwenAuthPlugin = async (_input: unknown) => {
9698 }
9799 }
98100
99- const accessToken = await getValidAccessToken ( getAuth ) ;
100- if ( ! accessToken ) return null ;
101+ // Get latest valid credentials
102+ const credentials = await tokenManager . getValidCredentials ( ) ;
103+ if ( ! credentials ?. accessToken ) return null ;
101104
102- // Load credentials to resolve region-specific base URL
103- const creds = loadCredentials ( ) ;
104- const baseURL = resolveBaseUrl ( creds ?. resource_url ) ;
105+ const baseURL = resolveBaseUrl ( credentials . resourceUrl ) ;
105106
106107 return {
107- apiKey : accessToken ,
108+ apiKey : credentials . accessToken ,
108109 baseURL : baseURL ,
109110 headers : {
110111 ...QWEN_OFFICIAL_HEADERS ,
111- // Custom metadata object required by official backend for free quota
112- 'X-Metadata' : JSON . stringify ( {
113- sessionId : PLUGIN_SESSION_ID ,
114- promptId : crypto . randomUUID ( ) ,
115- source : 'opencode-qwencode-auth'
116- } )
117112 } ,
118- // Custom fetch with throttling and retry
119- fetch : async ( url : string , options ?: RequestInit ) => {
113+ // Custom fetch with throttling, retry and 401 recovery
114+ fetch : async ( url : string , options : any = { } ) => {
120115 return requestQueue . enqueue ( async ( ) => {
121- return retryWithBackoff (
122- async ( ) => {
123- // Generate new promptId for each request
124- const headers = new Headers ( options ?. headers ) ;
125- headers . set ( 'Authorization' , `Bearer ${ accessToken } ` ) ;
126- headers . set (
127- 'X-Metadata' ,
128- JSON . stringify ( {
129- sessionId : PLUGIN_SESSION_ID ,
130- promptId : crypto . randomUUID ( ) ,
131- source : 'opencode-qwencode-auth' ,
132- } )
133- ) ;
134-
135- const response = await fetch ( url , {
136- ...options ,
137- headers,
138- } ) ;
139-
140- if ( ! response . ok ) {
141- const errorText = await response . text ( ) . catch ( ( ) => '' ) ;
142- const error = new Error ( `HTTP ${ response . status } : ${ errorText } ` ) as HttpError & { status ?: number } ;
143- error . status = response . status ;
144- ( error as any ) . response = response ;
145- throw error ;
116+ let authRetryCount = 0 ;
117+
118+ const executeRequest = async ( ) : Promise < Response > => {
119+ // Get latest token (possibly refreshed by concurrent request)
120+ const currentCreds = await tokenManager . getValidCredentials ( ) ;
121+ const token = currentCreds ?. accessToken ;
122+
123+ if ( ! token ) throw new Error ( 'No access token available' ) ;
124+
125+ // Prepare merged headers
126+ const mergedHeaders : Record < string , string > = {
127+ ...QWEN_OFFICIAL_HEADERS ,
128+ } ;
129+
130+ // Merge provided headers (handles both plain object and Headers instance)
131+ if ( options . headers ) {
132+ if ( typeof ( options . headers as any ) . entries === 'function' ) {
133+ for ( const [ k , v ] of ( options . headers as any ) . entries ( ) ) {
134+ const kl = k . toLowerCase ( ) ;
135+ if ( ! kl . startsWith ( 'x-dashscope' ) && kl !== 'user-agent' && kl !== 'authorization' ) {
136+ mergedHeaders [ k ] = v ;
137+ }
138+ }
139+ } else {
140+ for ( const [ k , v ] of Object . entries ( options . headers ) ) {
141+ const kl = k . toLowerCase ( ) ;
142+ if ( ! kl . startsWith ( 'x-dashscope' ) && kl !== 'user-agent' && kl !== 'authorization' ) {
143+ mergedHeaders [ k ] = v as string ;
144+ }
145+ }
146146 }
147+ }
147148
148- return response ;
149- } ,
150- {
151- authType : 'qwen-oauth' ,
152- maxAttempts : 7 ,
153- initialDelayMs : 1500 ,
154- maxDelayMs : 30000 ,
149+ // Force our Authorization token
150+ mergedHeaders [ 'Authorization' ] = `Bearer ${ token } ` ;
151+
152+ // Optional: X-Metadata might be expected by some endpoints for free quota tracking
153+ // but let's try without it first to match official client closer
154+ // mergedHeaders['X-Metadata'] = JSON.stringify({ ... });
155+
156+ // Perform the request
157+ const response = await fetch ( url , {
158+ ...options ,
159+ headers : mergedHeaders
160+ } ) ;
161+
162+ // Reactive recovery for 401 (token expired mid-session)
163+ if ( response . status === 401 && authRetryCount < 1 ) {
164+ authRetryCount ++ ;
165+ debugLogger . warn ( '401 Unauthorized detected. Forcing token refresh...' ) ;
166+
167+ // Force refresh from API
168+ const refreshed = await tokenManager . getValidCredentials ( true ) ;
169+ if ( refreshed ?. accessToken ) {
170+ debugLogger . info ( 'Token refreshed, retrying request...' ) ;
171+ return executeRequest ( ) ; // Recursive retry with new token
172+ }
155173 }
156- ) ;
174+
175+ // Error handling for retryWithBackoff
176+ if ( ! response . ok ) {
177+ const errorText = await response . text ( ) . catch ( ( ) => '' ) ;
178+ const error : any = new Error ( `HTTP ${ response . status } : ${ errorText } ` ) ;
179+ error . status = response . status ;
180+ throw error ;
181+ }
182+
183+ return response ;
184+ } ;
185+
186+ // Use official retry logic for 429/5xx errors
187+ return retryWithBackoff ( ( ) => executeRequest ( ) , {
188+ authType : 'qwen-oauth' ,
189+ maxAttempts : 7 ,
190+ shouldRetryOnError : ( error : any ) => {
191+ const status = error . status || getErrorStatus ( error ) ;
192+ // Retry on 401 (handled by executeRequest recursion too), 429, and 5xx
193+ return status === 401 || status === 429 || ( status !== undefined && status >= 500 && status < 600 ) ;
194+ }
195+ } ) ;
157196 } ) ;
158- } ,
197+ }
159198 } ;
160199 } ,
161200
@@ -189,7 +228,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
189228
190229 if ( tokenResponse ) {
191230 const credentials = tokenResponseToCredentials ( tokenResponse ) ;
192- saveCredentials ( credentials ) ;
231+ tokenManager . setCredentials ( credentials ) ;
193232
194233 return {
195234 type : 'success' as const ,
0 commit comments