@@ -56,11 +56,50 @@ export async function handleAiRequest(args: {
5656 ) ;
5757 } catch ( err ) {
5858 console . error ( "[api/ai] turn failed:" , err ) ;
59- sink . send ( "error" , { code : "server_error" , message : ( err as Error ) . message } ) ;
59+ sink . send ( "error" , describeTurnError ( err ) ) ;
6060 sink . end ( ) ;
6161 }
6262}
6363
64+ // Reads the `retry-after` header (seconds) from an upstream error, tolerating
65+ // both a Headers object and a plain record.
66+ function retryAfterSeconds ( err : unknown ) : number | undefined {
67+ const headers = ( err as { headers ?: unknown } ) . headers ;
68+ let raw : string | null | undefined ;
69+ if ( headers && typeof ( headers as Headers ) . get === "function" ) {
70+ raw = ( headers as Headers ) . get ( "retry-after" ) ;
71+ } else if ( headers && typeof headers === "object" ) {
72+ raw = ( headers as Record < string , string > ) [ "retry-after" ] ;
73+ }
74+ const n = Number ( raw ) ;
75+ return Number . isFinite ( n ) && n > 0 ? n : undefined ;
76+ }
77+
78+ // Maps an error thrown during the turn to a client-facing SSE error. Upstream
79+ // rate limits and overloads get a clear, actionable message (and retry timing)
80+ // instead of an opaque failure.
81+ export function describeTurnError ( err : unknown ) : {
82+ code : string ;
83+ message : string ;
84+ retryAfter ?: number ;
85+ } {
86+ const status = ( err as { status ?: number } ) . status ;
87+ if ( status === 429 ) {
88+ return {
89+ code : "rate_limit" ,
90+ message : "The AI service is rate limited. Please wait a moment and try again." ,
91+ retryAfter : retryAfterSeconds ( err ) ,
92+ } ;
93+ }
94+ if ( status === 503 || status === 529 ) {
95+ return {
96+ code : "overloaded" ,
97+ message : "The AI service is temporarily overloaded. Please try again shortly." ,
98+ } ;
99+ }
100+ return { code : "server_error" , message : ( err as Error ) . message || "The request failed." } ;
101+ }
102+
64103function clientIp ( req : VercelRequest ) : string {
65104 const realIp = req . headers [ "x-real-ip" ] ;
66105 if ( typeof realIp === "string" && realIp . trim ( ) ) return realIp . trim ( ) ;
0 commit comments