88const REAUTH_HINT =
99 'Execute "opencode auth login" e selecione "Qwen Code (qwen.ai OAuth)" para autenticar.' ;
1010
11+ // ============================================
12+ // Token Manager Error Types
13+ // ============================================
14+
15+ /**
16+ * Error types for token manager operations
17+ * Mirrors official client's TokenError enum
18+ */
19+ export enum TokenError {
20+ REFRESH_FAILED = 'REFRESH_FAILED' ,
21+ NO_REFRESH_TOKEN = 'NO_REFRESH_TOKEN' ,
22+ LOCK_TIMEOUT = 'LOCK_TIMEOUT' ,
23+ FILE_ACCESS_ERROR = 'FILE_ACCESS_ERROR' ,
24+ NETWORK_ERROR = 'NETWORK_ERROR' ,
25+ CREDENTIALS_CLEAR_REQUIRED = 'CREDENTIALS_CLEAR_REQUIRED' ,
26+ }
27+
1128// ============================================
1229// Erro de Autenticação
1330// ============================================
1431
15- export type AuthErrorKind = 'token_expired' | 'refresh_failed' | 'auth_required' ;
32+ export type AuthErrorKind = 'token_expired' | 'refresh_failed' | 'auth_required' | 'credentials_clear_required' ;
1633
1734const AUTH_MESSAGES : Record < AuthErrorKind , string > = {
1835 token_expired : `[Qwen] Token expirado. ${ REAUTH_HINT } ` ,
1936 refresh_failed : `[Qwen] Falha ao renovar token. ${ REAUTH_HINT } ` ,
2037 auth_required : `[Qwen] Autenticacao necessaria. ${ REAUTH_HINT } ` ,
38+ credentials_clear_required : `[Qwen] Credenciais invalidas ou revogadas. ${ REAUTH_HINT } ` ,
2139} ;
2240
2341export class QwenAuthError extends Error {
@@ -32,31 +50,97 @@ export class QwenAuthError extends Error {
3250 }
3351}
3452
53+ /**
54+ * Erro especial que sinaliza necessidade de limpar credenciais em cache.
55+ * Ocorre quando refresh token é revogado (invalid_grant).
56+ */
57+ export class CredentialsClearRequiredError extends QwenAuthError {
58+ constructor ( technicalDetail ?: string ) {
59+ super ( 'credentials_clear_required' , technicalDetail ) ;
60+ this . name = 'CredentialsClearRequiredError' ;
61+ }
62+ }
63+
64+ /**
65+ * Custom error class for token manager operations
66+ * Provides better error classification for handling
67+ */
68+ export class TokenManagerError extends Error {
69+ public readonly type : TokenError ;
70+ public readonly technicalDetail ?: string ;
71+
72+ constructor ( type : TokenError , message : string , technicalDetail ?: string ) {
73+ super ( message ) ;
74+ this . name = 'TokenManagerError' ;
75+ this . type = type ;
76+ this . technicalDetail = technicalDetail ;
77+ }
78+ }
79+
3580// ============================================
3681// Erro de API
3782// ============================================
3883
39- function classifyApiStatus ( statusCode : number ) : string {
84+ /**
85+ * Specific error types for API errors
86+ */
87+ export type ApiErrorKind =
88+ | 'rate_limit'
89+ | 'unauthorized'
90+ | 'forbidden'
91+ | 'server_error'
92+ | 'network_error'
93+ | 'unknown' ;
94+
95+ function classifyApiStatus ( statusCode : number ) : { message : string ; kind : ApiErrorKind } {
4096 if ( statusCode === 401 || statusCode === 403 ) {
41- return `[Qwen] Token invalido ou expirado. ${ REAUTH_HINT } ` ;
97+ return {
98+ message : `[Qwen] Token invalido ou expirado. ${ REAUTH_HINT } ` ,
99+ kind : 'unauthorized'
100+ } ;
42101 }
43102 if ( statusCode === 429 ) {
44- return '[Qwen] Limite de requisicoes atingido. Aguarde alguns minutos antes de tentar novamente.' ;
103+ return {
104+ message : '[Qwen] Limite de requisicoes atingido. Aguarde alguns minutos antes de tentar novamente.' ,
105+ kind : 'rate_limit'
106+ } ;
45107 }
46108 if ( statusCode >= 500 ) {
47- return `[Qwen] Servidor Qwen indisponivel (erro ${ statusCode } ). Tente novamente em alguns minutos.` ;
109+ return {
110+ message : `[Qwen] Servidor Qwen indisponivel (erro ${ statusCode } ). Tente novamente em alguns minutos.` ,
111+ kind : 'server_error'
112+ } ;
48113 }
49- return `[Qwen] Erro na API Qwen (${ statusCode } ). Verifique sua conexao e tente novamente.` ;
114+ return {
115+ message : `[Qwen] Erro na API Qwen (${ statusCode } ). Verifique sua conexao e tente novamente.` ,
116+ kind : 'unknown'
117+ } ;
50118}
51119
52120export class QwenApiError extends Error {
53121 public readonly statusCode : number ;
122+ public readonly kind : ApiErrorKind ;
54123 public readonly technicalDetail ?: string ;
55124
56125 constructor ( statusCode : number , technicalDetail ?: string ) {
57- super ( classifyApiStatus ( statusCode ) ) ;
126+ const classification = classifyApiStatus ( statusCode ) ;
127+ super ( classification . message ) ;
58128 this . name = 'QwenApiError' ;
59129 this . statusCode = statusCode ;
130+ this . kind = classification . kind ;
131+ this . technicalDetail = technicalDetail ;
132+ }
133+ }
134+
135+ /**
136+ * Error for network-related issues (fetch failures, timeouts, etc.)
137+ */
138+ export class QwenNetworkError extends Error {
139+ public readonly technicalDetail ?: string ;
140+
141+ constructor ( message : string , technicalDetail ?: string ) {
142+ super ( `[Qwen] Erro de rede: ${ message } ` ) ;
143+ this . name = 'QwenNetworkError' ;
60144 this . technicalDetail = technicalDetail ;
61145 }
62146}
@@ -73,3 +157,59 @@ export function logTechnicalDetail(detail: string): void {
73157 console . debug ( '[Qwen Debug]' , detail ) ;
74158 }
75159}
160+
161+ /**
162+ * Classify error type for better error handling
163+ * Returns specific error kind for programmatic handling
164+ */
165+ export function classifyError ( error : unknown ) : {
166+ kind : 'auth' | 'api' | 'network' | 'timeout' | 'unknown' ;
167+ isRetryable : boolean ;
168+ shouldClearCache : boolean ;
169+ } {
170+ // Check for our custom error types
171+ if ( error instanceof CredentialsClearRequiredError ) {
172+ return { kind : 'auth' , isRetryable : false , shouldClearCache : true } ;
173+ }
174+
175+ if ( error instanceof QwenAuthError ) {
176+ return {
177+ kind : 'auth' ,
178+ isRetryable : error . kind === 'refresh_failed' ,
179+ shouldClearCache : error . kind === 'credentials_clear_required'
180+ } ;
181+ }
182+
183+ if ( error instanceof QwenApiError ) {
184+ return {
185+ kind : 'api' ,
186+ isRetryable : error . kind === 'rate_limit' || error . kind === 'server_error' ,
187+ shouldClearCache : false
188+ } ;
189+ }
190+
191+ if ( error instanceof QwenNetworkError ) {
192+ return { kind : 'network' , isRetryable : true , shouldClearCache : false } ;
193+ }
194+
195+ // Check for timeout errors
196+ if ( error instanceof Error && error . name === 'AbortError' ) {
197+ return { kind : 'timeout' , isRetryable : true , shouldClearCache : false } ;
198+ }
199+
200+ // Check for standard Error with status
201+ if ( error instanceof Error ) {
202+ const errorMessage = error . message . toLowerCase ( ) ;
203+
204+ // Network-related errors
205+ if ( errorMessage . includes ( 'fetch' ) ||
206+ errorMessage . includes ( 'network' ) ||
207+ errorMessage . includes ( 'timeout' ) ||
208+ errorMessage . includes ( 'abort' ) ) {
209+ return { kind : 'network' , isRetryable : true , shouldClearCache : false } ;
210+ }
211+ }
212+
213+ // Default: unknown error, not retryable
214+ return { kind : 'unknown' , isRetryable : false , shouldClearCache : false } ;
215+ }
0 commit comments