@@ -36,6 +36,8 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
3636 protected readonly options : ApiHandlerOptions
3737
3838 protected client : OpenAI
39+ // Abort controller for cancelling ongoing requests (fixes #404)
40+ private abortController ?: AbortController
3941
4042 constructor ( {
4143 providerName,
@@ -115,87 +117,103 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
115117 messages : Anthropic . Messages . MessageParam [ ] ,
116118 metadata ?: ApiHandlerCreateMessageMetadata ,
117119 ) : ApiStream {
118- const stream = await this . createStream ( systemPrompt , messages , metadata )
120+ // Create AbortController for cancellation (fixes #404)
121+ this . abortController = new AbortController ( )
119122
120- const matcher = new TagMatcher (
121- "think" ,
122- ( chunk ) =>
123- ( {
124- type : chunk . matched ? "reasoning" : "text" ,
125- text : chunk . data ,
126- } ) as const ,
127- )
128-
129- let lastUsage : OpenAI . CompletionUsage | undefined
130- const activeToolCallIds = new Set < string > ( )
123+ try {
124+ const stream = await this . createStream ( systemPrompt , messages , metadata , {
125+ signal : this . abortController . signal ,
126+ } )
127+
128+ const matcher = new TagMatcher (
129+ "think" ,
130+ ( chunk ) =>
131+ ( {
132+ type : chunk . matched ? "reasoning" : "text" ,
133+ text : chunk . data ,
134+ } ) as const ,
135+ )
136+
137+ let lastUsage : OpenAI . CompletionUsage | undefined
138+ const activeToolCallIds = new Set < string > ( )
139+
140+ for await ( const chunk of stream ) {
141+ // Check if request was aborted (fixes #404)
142+ if ( this . abortController ?. signal . aborted ) {
143+ break
144+ }
131145
132- for await ( const chunk of stream ) {
133- // Check for provider-specific error responses (e.g., MiniMax base_resp)
134- const chunkAny = chunk as any
135- if ( chunkAny . base_resp ?. status_code && chunkAny . base_resp . status_code !== 0 ) {
136- throw new Error (
137- `${ this . providerName } API Error (${ chunkAny . base_resp . status_code } ): ${ chunkAny . base_resp . status_msg || "Unknown error" } ` ,
138- )
139- }
146+ // Check for provider-specific error responses (e.g., MiniMax base_resp)
147+ const chunkAny = chunk as any
148+ if ( chunkAny . base_resp ?. status_code && chunkAny . base_resp . status_code !== 0 ) {
149+ throw new Error (
150+ `${ this . providerName } API Error (${ chunkAny . base_resp . status_code } ): ${ chunkAny . base_resp . status_msg || "Unknown error" } ` ,
151+ )
152+ }
140153
141- const delta = chunk . choices ?. [ 0 ] ?. delta
142- const finishReason = chunk . choices ?. [ 0 ] ?. finish_reason
154+ const delta = chunk . choices ?. [ 0 ] ?. delta
155+ const finishReason = chunk . choices ?. [ 0 ] ?. finish_reason
143156
144- if ( delta ?. content ) {
145- for ( const processedChunk of matcher . update ( delta . content ) ) {
146- yield processedChunk
157+ if ( delta ?. content ) {
158+ for ( const processedChunk of matcher . update ( delta . content ) ) {
159+ yield processedChunk
160+ }
147161 }
148- }
149162
150- if ( delta ) {
151- for ( const key of [ "reasoning_content" , "reasoning" ] as const ) {
152- if ( key in delta ) {
153- const reasoning_content = ( ( delta as any ) [ key ] as string | undefined ) || ""
154- if ( reasoning_content ?. trim ( ) ) {
155- yield { type : "reasoning" , text : reasoning_content }
163+ if ( delta ) {
164+ for ( const key of [ "reasoning_content" , "reasoning" ] as const ) {
165+ if ( key in delta ) {
166+ const reasoning_content = ( ( delta as any ) [ key ] as string | undefined ) || ""
167+ if ( reasoning_content ?. trim ( ) ) {
168+ yield { type : "reasoning" , text : reasoning_content }
169+ }
170+ break
156171 }
157- break
158172 }
159173 }
160- }
161174
162- // Emit raw tool call chunks - NativeToolCallParser handles state management
163- if ( delta ?. tool_calls ) {
164- for ( const toolCall of delta . tool_calls ) {
165- if ( toolCall . id ) {
166- activeToolCallIds . add ( toolCall . id )
175+ // Emit raw tool call chunks - NativeToolCallParser handles state management
176+ if ( delta ?. tool_calls ) {
177+ for ( const toolCall of delta . tool_calls ) {
178+ if ( toolCall . id ) {
179+ activeToolCallIds . add ( toolCall . id )
180+ }
181+ yield {
182+ type : "tool_call_partial" ,
183+ index : toolCall . index ,
184+ id : toolCall . id ,
185+ name : toolCall . function ?. name ,
186+ arguments : toolCall . function ?. arguments ,
187+ }
167188 }
168- yield {
169- type : "tool_call_partial" ,
170- index : toolCall . index ,
171- id : toolCall . id ,
172- name : toolCall . function ?. name ,
173- arguments : toolCall . function ?. arguments ,
189+ }
190+
191+ // Emit tool_call_end events when finish_reason is "tool_calls"
192+ // This ensures tool calls are finalized even if the stream doesn't properly close
193+ if ( finishReason === "tool_calls" && activeToolCallIds . size > 0 ) {
194+ for ( const id of activeToolCallIds ) {
195+ yield { type : "tool_call_end" , id }
174196 }
197+ activeToolCallIds . clear ( )
175198 }
176- }
177199
178- // Emit tool_call_end events when finish_reason is "tool_calls"
179- // This ensures tool calls are finalized even if the stream doesn't properly close
180- if ( finishReason === "tool_calls" && activeToolCallIds . size > 0 ) {
181- for ( const id of activeToolCallIds ) {
182- yield { type : "tool_call_end" , id }
200+ if ( chunk . usage ) {
201+ lastUsage = chunk . usage
183202 }
184- activeToolCallIds . clear ( )
185203 }
186204
187- if ( chunk . usage ) {
188- lastUsage = chunk . usage
205+ if ( lastUsage ) {
206+ yield this . processUsageMetrics ( lastUsage , this . getModel ( ) . info )
189207 }
190- }
191208
192- if ( lastUsage ) {
193- yield this . processUsageMetrics ( lastUsage , this . getModel ( ) . info )
194- }
195-
196- // Process any remaining content
197- for ( const processedChunk of matcher . final ( ) ) {
198- yield processedChunk
209+ // Process any remaining content
210+ for ( const processedChunk of matcher . final ( ) ) {
211+ yield processedChunk
212+ }
213+ } catch ( error ) {
214+ throw handleOpenAIError ( error , this . providerName )
215+ } finally {
216+ this . abortController = undefined
199217 }
200218 }
201219
@@ -222,6 +240,9 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
222240 async completePrompt ( prompt : string ) : Promise < string > {
223241 const { id : modelId , info : modelInfo } = this . getModel ( )
224242
243+ // Create AbortController for cancellation (fixes #404)
244+ this . abortController = new AbortController ( )
245+
225246 const params : OpenAI . Chat . Completions . ChatCompletionCreateParams = {
226247 model : modelId ,
227248 messages : [ { role : "user" , content : prompt } ] ,
@@ -233,7 +254,9 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
233254 }
234255
235256 try {
236- const response = await this . client . chat . completions . create ( params )
257+ const response = await this . client . chat . completions . create ( params , {
258+ signal : this . abortController . signal ,
259+ } )
237260
238261 // Check for provider-specific error responses (e.g., MiniMax base_resp)
239262 const responseAny = response as any
@@ -246,6 +269,8 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
246269 return response . choices ?. [ 0 ] ?. message . content || ""
247270 } catch ( error ) {
248271 throw handleOpenAIError ( error , this . providerName )
272+ } finally {
273+ this . abortController = undefined
249274 }
250275 }
251276
0 commit comments