@@ -70,6 +70,21 @@ function isRetryableError(status: number): boolean {
7070 return false
7171}
7272
73+ const GCP_PERMISSION_ERROR_PATTERNS = [
74+ "PERMISSION_DENIED" ,
75+ "does not have permission" ,
76+ "Cloud AI Companion API has not been used" ,
77+ "has not been enabled" ,
78+ ] as const
79+
80+ function isGcpPermissionError ( text : string ) : boolean {
81+ return GCP_PERMISSION_ERROR_PATTERNS . some ( ( pattern ) => text . includes ( pattern ) )
82+ }
83+
84+ function calculateRetryDelay ( attempt : number ) : number {
85+ return Math . min ( 200 * Math . pow ( 2 , attempt ) , 2000 )
86+ }
87+
7388async function isRetryableResponse ( response : Response ) : Promise < boolean > {
7489 if ( isRetryableError ( response . status ) ) return true
7590 if ( response . status === 403 ) {
@@ -155,23 +170,43 @@ async function attemptFetch(
155170
156171 debugLog ( `[REQ] streaming=${ transformed . streaming } , url=${ transformed . url } ` )
157172
158- const response = await fetch ( transformed . url , {
159- method : init . method || "POST" ,
160- headers : transformed . headers ,
161- body : JSON . stringify ( transformed . body ) ,
162- signal : init . signal ,
163- } )
173+ const maxPermissionRetries = 10
174+ for ( let attempt = 0 ; attempt <= maxPermissionRetries ; attempt ++ ) {
175+ const response = await fetch ( transformed . url , {
176+ method : init . method || "POST" ,
177+ headers : transformed . headers ,
178+ body : JSON . stringify ( transformed . body ) ,
179+ signal : init . signal ,
180+ } )
164181
165- debugLog (
166- `[RESP] status=${ response . status } content-type=${ response . headers . get ( "content-type" ) ?? "" } url=${ response . url } `
167- )
182+ debugLog (
183+ `[RESP] status=${ response . status } content-type=${ response . headers . get ( "content-type" ) ?? "" } url=${ response . url } `
184+ )
185+
186+ if ( response . status === 403 ) {
187+ try {
188+ const text = await response . clone ( ) . text ( )
189+ if ( isGcpPermissionError ( text ) ) {
190+ if ( attempt < maxPermissionRetries ) {
191+ const delay = calculateRetryDelay ( attempt )
192+ debugLog ( `[RETRY] GCP permission error, retry ${ attempt + 1 } /${ maxPermissionRetries } after ${ delay } ms` )
193+ await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) )
194+ continue
195+ }
196+ debugLog ( `[RETRY] GCP permission error, max retries exceeded` )
197+ }
198+ } catch { }
199+ }
168200
169- if ( ! response . ok && ( await isRetryableResponse ( response ) ) ) {
170- debugLog ( `Endpoint failed: ${ endpoint } (status: ${ response . status } ), trying next` )
171- return null
201+ if ( ! response . ok && ( await isRetryableResponse ( response ) ) ) {
202+ debugLog ( `Endpoint failed: ${ endpoint } (status: ${ response . status } ), trying next` )
203+ return null
204+ }
205+
206+ return response
172207 }
173208
174- return response
209+ return null
175210 } catch ( error ) {
176211 debugLog (
177212 `Endpoint failed: ${ endpoint } (${ error instanceof Error ? error . message : "Unknown error" } ), trying next`
0 commit comments