@@ -67,20 +67,38 @@ function sanitizeForJson(obj: unknown): unknown {
6767 * Mask sensitive values in HTTP headers before logging.
6868 * "Bearer sk-1234567890abcdef" → "Bearer sk-****cdef"
6969 */
70- function maskSensitiveHeaders ( headers : Record < string , string > ) : Record < string , string > {
71- const masked = { ...headers } ;
72- for ( const key of Object . keys ( masked ) ) {
73- const lower = key . toLowerCase ( ) ;
70+ function maskSensitiveHeaders ( headers : Record < string , string > ) : Record < string , string > {
71+ const masked = { ...headers } ;
72+ for ( const key of Object . keys ( masked ) ) {
73+ const lower = key . toLowerCase ( ) ;
7474 if ( lower === 'authorization' || lower === 'x-api-key' || lower === 'api-key' ) {
7575 masked [ key ] = masked [ key ] . replace ( / ( \w { 2 , 4 } ) \w { 4 , } ( \w { 4 } ) / , '$1****$2' ) ;
7676 }
7777 }
78- return masked ;
79- }
80-
81- /**
82- * LLMRouter — Unified interface for calling different LLM providers.
83- * Supports OpenAI, Anthropic, and OpenAI-compatible APIs.
78+ return masked ;
79+ }
80+
81+ function delayWithAbort ( ms : number , signal ?: AbortSignal ) : Promise < void > {
82+ if ( signal ?. aborted ) return Promise . reject ( signal . reason ) ;
83+
84+ return new Promise ( ( resolve , reject ) => {
85+ const cleanup = ( ) : void => signal ?. removeEventListener ( "abort" , abort ) ;
86+ const timeout = setTimeout ( ( ) => {
87+ cleanup ( ) ;
88+ resolve ( ) ;
89+ } , ms ) ;
90+ const abort = ( ) : void => {
91+ clearTimeout ( timeout ) ;
92+ cleanup ( ) ;
93+ reject ( signal ?. reason ) ;
94+ } ;
95+ signal ?. addEventListener ( "abort" , abort , { once : true } ) ;
96+ } ) ;
97+ }
98+
99+ /**
100+ * LLMRouter — Unified interface for calling different LLM providers.
101+ * Supports OpenAI, Anthropic, and OpenAI-compatible APIs.
84102 */
85103export class LLMRouter {
86104 constructor (
@@ -111,10 +129,15 @@ export class LLMRouter {
111129 throw new Error ( `LLM API 错误: ${ err . type ?? 'unknown' } — ${ err . message ?? JSON . stringify ( err ) } ` ) ;
112130 }
113131
114- // OpenAI error format: { error: { message, type, code } }
115- if ( typeof obj . error === 'object' && obj . error !== null && ! obj . type ) {
116- const err = obj . error as Record < string , unknown > ;
117- throw new Error ( `LLM API 错误: ${ err . message ?? JSON . stringify ( err ) } ` ) ;
132+ // Responses API failed payloads need endpoint-specific handling.
133+ if ( obj . status === "failed" ) {
134+ return data as T ;
135+ }
136+
137+ // OpenAI error format: { error: { message, type, code } }
138+ if ( typeof obj . error === 'object' && obj . error !== null && ! obj . type ) {
139+ const err = obj . error as Record < string , unknown > ;
140+ throw new Error ( `LLM API 错误: ${ err . message ?? JSON . stringify ( err ) } ` ) ;
118141 }
119142
120143 return data as T ;
@@ -573,18 +596,22 @@ export class LLMRouter {
573596
574597 if ( stream ) return this . parseResponsesStream ( response , onChunk ! ) ;
575598
576- const data = await this . safeParseJson < {
599+ const data = await this . safeParseJson < {
577600 status ?: string ;
578601 incomplete_details ?: ResponsesIncompleteDetails ;
579- output_text ?: string ;
580- usage ?: { input_tokens : number ; output_tokens : number } ;
581- } > ( response ) ;
602+ error ?: { message ?: string } ;
603+ output_text ?: string ;
604+ usage ?: { input_tokens : number ; output_tokens : number } ;
605+ } > ( response ) ;
582606 if ( data . status === "incomplete" ) {
583607 throw new Error ( `Responses API incomplete: ${ data . incomplete_details ?. reason || "unknown" } ` ) ;
584608 }
585- return {
586- content : data . output_text || "" ,
587- promptTokens : data . usage ?. input_tokens || 0 ,
609+ if ( data . status === "failed" ) {
610+ throw new Error ( `Responses API failed: ${ data . error ?. message || "unknown" } ` ) ;
611+ }
612+ return {
613+ content : data . output_text || "" ,
614+ promptTokens : data . usage ?. input_tokens || 0 ,
588615 completionTokens : data . usage ?. output_tokens || 0 ,
589616 } ;
590617 }
@@ -833,12 +860,12 @@ export class LLMRouter {
833860 } ) ;
834861 clearTimeout ( timeout ) ;
835862
836- if ( response . status === 429 && retries > 0 ) {
863+ if ( response . status === 429 && retries > 0 ) {
837864 const retryAfter = parseInt (
838865 response . headers . get ( "retry-after" ) || "5" ,
839866 10 ,
840867 ) ;
841- await new Promise ( ( r ) => setTimeout ( r , retryAfter * 1000 ) ) ;
868+ await delayWithAbort ( retryAfter * 1000 , signal ) ;
842869 return this . fetchWithRetry ( url , options , retries - 1 , isStreaming , signal ) ;
843870 }
844871
0 commit comments