1- import type { OpenAI } from '../..' ;
1+ import { OpenAI } from '../..' ;
22import { OpenAIError } from '../../core/error' ;
33import { buildHeaders } from '../../internal/headers' ;
44import type { RequestOptions } from '../../internal/request-options' ;
55import type {
66 ChatCompletion ,
77 ChatCompletionCreateParams ,
8+ ChatCompletionMessage ,
89 ChatCompletionMessageParam ,
910 ChatCompletionStream ,
1011 ChatCompletionTool ,
@@ -45,14 +46,18 @@ export class BetaToolRunner<Stream extends boolean> {
4546 /** Current state containing the request parameters */
4647 #state: { params : BetaToolRunnerParams } ;
4748 #options: BetaToolRunnerRequestOptions ;
48- /** Promise for the last message received from the assistant */
49- #message?: Promise < ChatCompletion > | undefined ;
49+ /**
50+ * Promise for the last message received from the assistant.
51+ *
52+ * This resolves to undefined in non-streaming mode if there are no choices provided.
53+ */
54+ #message?: Promise < ChatCompletionMessage > | undefined ;
5055 /** Cached tool response to avoid redundant executions */
5156 #toolResponse?: Promise < null | ChatCompletionToolMessageParam [ ] > | undefined ;
5257 /** Promise resolvers for waiting on completion */
5358 #completion: {
54- promise : Promise < ChatCompletion > ;
55- resolve : ( value : ChatCompletion ) => void ;
59+ promise : Promise < ChatCompletionMessage > ;
60+ resolve : ( value : ChatCompletionMessage ) => void ;
5661 reject : ( reason ?: any ) => void ;
5762 } ;
5863 /** Number of iterations (API requests) made so far */
@@ -83,8 +88,7 @@ export class BetaToolRunner<Stream extends boolean> {
8388 async * [ Symbol . asyncIterator ] ( ) : AsyncIterator <
8489 Stream extends true ?
8590 ChatCompletionStream // TODO: for now!
86- : Stream extends false ? ChatCompletion
87- : ChatCompletionCreateParams | ChatCompletionCreateParams
91+ : ChatCompletion | undefined
8892 > {
8993 if ( this . #consumed) {
9094 throw new OpenAIError ( 'Cannot iterate over a consumed stream' ) ;
@@ -96,7 +100,7 @@ export class BetaToolRunner<Stream extends boolean> {
96100
97101 try {
98102 while ( true ) {
99- let stream : any ;
103+ let stream : ChatCompletionStream < null > | undefined ;
100104 try {
101105 if (
102106 this . #state. params . max_iterations &&
@@ -111,44 +115,48 @@ export class BetaToolRunner<Stream extends boolean> {
111115 this . #iterationCount++ ;
112116
113117 const { ...params } = this . #state. params ;
118+ const apiParams = { ...params } ;
119+ delete apiParams . max_iterations ; // our own param
120+
114121 if ( params . stream ) {
115- stream = this . client . beta . chat . completions . stream ( { ...params , stream : true } , this . #options) ;
122+ stream = this . client . beta . chat . completions . stream ( { ...apiParams , stream : true } , this . #options) ;
116123 this . #message = stream . finalMessage ( ) ;
124+
117125 // Make sure that this promise doesn't throw before we get the option to do something about it.
118126 // Error will be caught when we call await this.#message ultimately
119127 this . #message?. catch ( ( ) => { } ) ;
120128 yield stream as any ;
121129 } else {
122- this . #message = this . client . beta . chat . completions . create (
130+ const currentCompletion = this . client . beta . chat . completions . create (
123131 {
132+ ...apiParams , // spread and explicit so we get better types
124133 stream : false ,
125134 tools : params . tools ,
126135 messages : params . messages ,
127136 model : params . model ,
128137 } ,
129138 this . #options,
130139 ) ;
131- yield this . #message as any ;
132- }
133140
134- if ( ! this . #message) {
135- throw new Error ( 'No message created' ) ; // TODO: use better error
141+ yield currentCompletion as any ;
142+
143+ this . #message = currentCompletion . then ( ( resp ) => resp . choices . at ( 0 ) ! . message ) ;
136144 }
137145
138- // TODO: we should probably hit the user with a callback or somehow offer for them to choice between the choices
139- if ( ! this . #message) {
140- throw new Error ( 'No choices found in message' ) ; // TODO: use better error
146+ const prevMessage = await this . #message;
147+
148+ if ( ! prevMessage ) {
149+ throw new OpenAIError ( 'ToolRunner concluded without a message from the server' ) ;
141150 }
142151
143152 if ( ! this . #mutated) {
144- const completion = await this . #message;
145153 // TODO: what if it is empty?
146- if ( completion ?. choices && completion . choices . length > 0 && completion . choices [ 0 ] ! . message ) {
147- this . #state. params . messages . push ( completion . choices [ 0 ] ! . message ) ;
154+ if ( prevMessage ) {
155+ this . #state. params . messages . push ( prevMessage ) ;
148156 }
149157 }
150158
151- const toolMessages = await this . #generateToolResponse( await this . #message ) ;
159+ const toolMessages = await this . #generateToolResponse( prevMessage ) ;
152160 if ( toolMessages ) {
153161 for ( const toolMessage of toolMessages ) {
154162 this . #state. params . messages . push ( toolMessage ) ;
@@ -228,15 +236,16 @@ export class BetaToolRunner<Stream extends boolean> {
228236 * }
229237 */
230238 async generateToolResponse ( ) {
231- const message = ( await this . #message) ?? this . params . messages . at ( - 1 ) ;
239+ // The most recent message from the assistant. This prev had this.params.messages.at(-1) but I think that's wrong.
240+ const message = await this . #message;
232241 if ( ! message ) {
233242 return null ;
234243 }
235244 return this . #generateToolResponse( message ) ;
236245 }
237246
238- async #generateToolResponse( lastMessage : ChatCompletion | ChatCompletionMessageParam ) {
239- if ( this . #toolResponse !== undefined ) {
247+ async #generateToolResponse( lastMessage : ChatCompletionMessage ) {
248+ if ( this . #toolResponse) {
240249 return this . #toolResponse;
241250 }
242251 const toolsResponse = generateToolResponse (
@@ -263,8 +272,8 @@ export class BetaToolRunner<Stream extends boolean> {
263272 * const finalMessage = await runner.done();
264273 * console.log('Final response:', finalMessage.content);
265274 */
266- done ( ) : Promise < ChatCompletion > {
267- return this . #completion. promise ;
275+ done ( ) : Promise < ChatCompletionMessage > {
276+ return this . #completion. promise ; // TODO: find a more type safe way to do this
268277 }
269278
270279 /**
@@ -280,7 +289,7 @@ export class BetaToolRunner<Stream extends boolean> {
280289 * const finalMessage = await runner.runUntilDone();
281290 * console.log('Final response:', finalMessage.content);
282291 */
283- async runUntilDone ( ) : Promise < ChatCompletion > {
292+ async runUntilDone ( ) : Promise < ChatCompletionMessage > {
284293 // If not yet consumed, start consuming and wait for completion
285294 if ( ! this . #consumed) {
286295 for await ( const _ of this ) {
@@ -334,27 +343,18 @@ export class BetaToolRunner<Stream extends boolean> {
334343 * Makes the ToolRunner directly awaitable, equivalent to calling .runUntilDone()
335344 * This allows using `await runner` instead of `await runner.runUntilDone()`
336345 */
337- then < TResult1 = ChatCompletion , TResult2 = never > (
338- onfulfilled ?: ( ( value : ChatCompletion ) => TResult1 | PromiseLike < TResult1 > ) | undefined | null ,
346+ then < TResult1 = ChatCompletionMessage , TResult2 = never > ( // TODO: make sure these types are OK
347+ onfulfilled ?: ( ( value : ChatCompletionMessage ) => TResult1 | PromiseLike < TResult1 > ) | undefined | null ,
339348 onrejected ?: ( ( reason : any ) => TResult2 | PromiseLike < TResult2 > ) | undefined | null ,
340349 ) : Promise < TResult1 | TResult2 > {
341350 return this . runUntilDone ( ) . then ( onfulfilled , onrejected ) ;
342351 }
343352}
344353
345354async function generateToolResponse (
346- params : ChatCompletion | ChatCompletionMessageParam ,
355+ lastMessage : ChatCompletionMessage ,
347356 tools : BetaRunnableTool < any > [ ] ,
348357) : Promise < null | ChatCompletionToolMessageParam [ ] > {
349- if ( ! ( 'choices' in params ) ) {
350- return null ;
351- }
352- const { choices } = params ;
353- const lastMessage = choices [ 0 ] ?. message ;
354- if ( ! lastMessage ) {
355- return null ;
356- }
357-
358358 // Only process if the last message is from the assistant and has tool use blocks
359359 if (
360360 ! lastMessage ||
0 commit comments