@@ -3760,37 +3760,46 @@ function claudeUsageLimitResetAt(resetsAt: unknown): string | null {
37603760 : null ;
37613761}
37623762
3763+ function gatewayCooldownResetAt (
3764+ cooldown : RegExpExecArray | null ,
3765+ completedAt : DateTime . Utc ,
3766+ ) : string | undefined {
3767+ if ( cooldown === null ) return undefined ;
3768+ const seconds =
3769+ Number ( cooldown [ 1 ] ?? 0 ) * 3_600 + Number ( cooldown [ 2 ] ?? 0 ) * 60 + Number ( cooldown [ 3 ] ?? 0 ) ;
3770+ const resetMs = DateTime . toEpochMillis ( completedAt ) + seconds * 1_000 ;
3771+ return seconds > 0 && resetMs < 8.64e15
3772+ ? DateTime . formatIso ( DateTime . makeUnsafe ( resetMs ) )
3773+ : undefined ;
3774+ }
3775+
37633776function providerFailureFromResult (
37643777 message : SDKResultMessage ,
37653778 completedAt : DateTime . Utc ,
37663779 failureHint ?: string ,
37673780 usageLimited = false ,
37683781) : OrchestrationV2ProviderFailure | null {
3769- const gatewayLimit =
3782+ const gatewayResetAt =
37703783 failureHint === undefined &&
37713784 message . subtype === "success" &&
37723785 message . is_error &&
37733786 message . terminal_reason === "api_error" &&
37743787 message . api_error_status === 503
3775- ? / ^ A P I E r r o r : 5 0 3 \[ [ ^ \] \r \n ] + \] \[ 4 2 9 \] : \{ " t y p e " : " e r r o r " , " e r r o r " : \{ " t y p e " : " r a t e _ l i m i t _ e r r o r " , [ ^ \r \n ] * \} \( r e s e t a f t e r (?: ( \d + ) h ? ) ? (?: ( \d + ) m ? ) ? (?: ( \d + ) s ) ? \) / . exec (
3776- message . result ,
3788+ ? gatewayCooldownResetAt (
3789+ / ^ A P I E r r o r : 5 0 3 \[ [ ^ \] \r \n ] + \] \[ 4 2 9 \] : \{ " t y p e " : " e r r o r " , " e r r o r " : \{ " t y p e " : " r a t e _ l i m i t _ e r r o r " , [ ^ \r \n ] * \} \( r e s e t a f t e r (?: ( \d + ) h ? ) ? (?: ( \d + ) m ? ) ? (?: ( \d + ) s ) ? \) / . exec (
3790+ message . result ,
3791+ ) ,
3792+ completedAt ,
37773793 )
3778- : null ;
3779- if ( gatewayLimit ) {
3780- const seconds =
3781- Number ( gatewayLimit [ 1 ] ?? 0 ) * 3_600 +
3782- Number ( gatewayLimit [ 2 ] ?? 0 ) * 60 +
3783- Number ( gatewayLimit [ 3 ] ?? 0 ) ;
3784- const resetMs = DateTime . toEpochMillis ( completedAt ) + seconds * 1_000 ;
3785- if ( seconds > 0 && resetMs < 8.64e15 ) {
3786- return makeProviderFailure ( {
3787- class : "usage_limit" ,
3788- message : "Claude API rate limit reached. Try again later." ,
3789- code : "api_error_429" ,
3790- retryable : true ,
3791- resetAt : DateTime . formatIso ( DateTime . makeUnsafe ( resetMs ) ) ,
3792- } ) ;
3793- }
3794+ : undefined ;
3795+ if ( gatewayResetAt !== undefined ) {
3796+ return makeProviderFailure ( {
3797+ class : "usage_limit" ,
3798+ message : "Claude API rate limit reached. Try again later." ,
3799+ code : "api_error_429" ,
3800+ retryable : true ,
3801+ resetAt : gatewayResetAt ,
3802+ } ) ;
37943803 }
37953804 const failureClass =
37963805 ( message . subtype === "success" && message . api_error_status === 429 ) || usageLimited
@@ -3819,6 +3828,14 @@ function providerFailureFromResult(
38193828 return null ;
38203829 }
38213830 const apiErrorStatus = message . api_error_status ?? null ;
3831+ // API-key sessions get no rate_limit_info, so a gateway's 429 carries its reset only as text.
3832+ const resetAt =
3833+ apiErrorStatus === 429 && message . result . startsWith ( "API Error:" )
3834+ ? gatewayCooldownResetAt (
3835+ / \( r e s e t a f t e r (?: ( \d + ) h ? ) ? (?: ( \d + ) m ? ) ? (?: ( \d + ) s ) ? \) / . exec ( message . result ) ,
3836+ completedAt ,
3837+ )
3838+ : undefined ;
38223839 return makeProviderFailure ( {
38233840 message : listedError ?? structuredError ?? failureHint ?? message . result ,
38243841 code :
@@ -3827,6 +3844,7 @@ function providerFailureFromResult(
38273844 : `api_error_${ apiErrorStatus } ` ,
38283845 class : failureClass ,
38293846 retryable : apiErrorStatus === 429 || apiErrorStatus === 529 ? true : null ,
3847+ resetAt : resetAt ?? null ,
38303848 } ) ;
38313849}
38323850
0 commit comments