1111using  System . Runtime . CompilerServices ; 
1212using  System . Text ; 
1313using  System . Text . Json ; 
14+ using  System . Text . Json . Nodes ; 
1415using  System . Threading ; 
1516using  System . Threading . Tasks ; 
1617using  Microsoft . Shared . Diagnostics ; 
1718using  OpenAI . Responses ; 
1819
20+ #pragma warning disable S1226  // Method parameters, caught exceptions and foreach variables' initial values should not be ignored 
1921#pragma warning disable S3011  // Reflection should not be used to increase accessibility of classes, methods, or fields 
2022#pragma warning disable S3254  // Default parameter values should not be passed as arguments 
2123#pragma warning disable SA1204  // Static elements should appear before instance elements 
@@ -85,14 +87,14 @@ public async Task<ChatResponse> GetResponseAsync(
8587        _  =  Throw . IfNull ( messages ) ; 
8688
8789        // Convert the inputs into what OpenAIResponseClient expects. 
88-         var  openAIOptions  =  ToOpenAIResponseCreationOptions ( options ) ; 
90+         var  openAIOptions  =  ToOpenAIResponseCreationOptions ( options ,   out   string ?   openAIConversationId ) ; 
8991
9092        // Provided continuation token signals that an existing background response should be fetched. 
9193        if  ( GetContinuationToken ( messages ,  options )  is  {  }  token ) 
9294        { 
9395            var  response  =  await  _responseClient . GetResponseAsync ( token . ResponseId ,  cancellationToken ) . ConfigureAwait ( false ) ; 
9496
95-             return  FromOpenAIResponse ( response ,  openAIOptions ) ; 
97+             return  FromOpenAIResponse ( response ,  openAIOptions ,   openAIConversationId ) ; 
9698        } 
9799
98100        var  openAIResponseItems  =  ToOpenAIResponseItems ( messages ,  options ) ; 
@@ -104,15 +106,15 @@ public async Task<ChatResponse> GetResponseAsync(
104106        var  openAIResponse  =  ( await  task . ConfigureAwait ( false ) ) . Value ; 
105107
106108        // Convert the response to a ChatResponse. 
107-         return  FromOpenAIResponse ( openAIResponse ,  openAIOptions ) ; 
109+         return  FromOpenAIResponse ( openAIResponse ,  openAIOptions ,   openAIConversationId ) ; 
108110    } 
109111
110-     internal  static ChatResponse  FromOpenAIResponse ( OpenAIResponse  openAIResponse ,  ResponseCreationOptions ?  openAIOptions ) 
112+     internal  static ChatResponse  FromOpenAIResponse ( OpenAIResponse  openAIResponse ,  ResponseCreationOptions ?  openAIOptions ,   string ?   conversationId ) 
111113    { 
112114        // Convert and return the results. 
113115        ChatResponse  response  =  new ( ) 
114116        { 
115-             ConversationId  =  openAIOptions ? . StoredOutputEnabled  is  false  ?  null  :  openAIResponse . Id , 
117+             ConversationId  =  openAIOptions ? . StoredOutputEnabled  is  false  ?  null  :  ( conversationId   ??   openAIResponse . Id ) , 
116118            CreatedAt  =  openAIResponse . CreatedAt , 
117119            ContinuationToken  =  CreateContinuationToken ( openAIResponse ) , 
118120            FinishReason  =  ToFinishReason ( openAIResponse . IncompleteStatusDetails ? . Reason ) , 
@@ -232,14 +234,14 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
232234    { 
233235        _  =  Throw . IfNull ( messages ) ; 
234236
235-         var  openAIOptions  =  ToOpenAIResponseCreationOptions ( options ) ; 
237+         var  openAIOptions  =  ToOpenAIResponseCreationOptions ( options ,   out   string ?   openAIConversationId ) ; 
236238
237239        // Provided continuation token signals that an existing background response should be fetched. 
238240        if  ( GetContinuationToken ( messages ,  options )  is  {  }  token ) 
239241        { 
240242            IAsyncEnumerable < StreamingResponseUpdate >  updates  =  _responseClient . GetResponseStreamingAsync ( token . ResponseId ,  token . SequenceNumber ,  cancellationToken ) ; 
241243
242-             return  FromOpenAIStreamingResponseUpdatesAsync ( updates ,  openAIOptions ,  token . ResponseId ,  cancellationToken ) ; 
244+             return  FromOpenAIStreamingResponseUpdatesAsync ( updates ,  openAIOptions ,  openAIConversationId ,   token . ResponseId ,  cancellationToken ) ; 
243245        } 
244246
245247        var  openAIResponseItems  =  ToOpenAIResponseItems ( messages ,  options ) ; 
@@ -248,24 +250,26 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
248250            _createResponseStreamingAsync ( _responseClient ,  openAIResponseItems ,  openAIOptions ,  cancellationToken . ToRequestOptions ( streaming :  true ) )  : 
249251            _responseClient . CreateResponseStreamingAsync ( openAIResponseItems ,  openAIOptions ,  cancellationToken ) ; 
250252
251-         return  FromOpenAIStreamingResponseUpdatesAsync ( streamingUpdates ,  openAIOptions ,  cancellationToken :  cancellationToken ) ; 
253+         return  FromOpenAIStreamingResponseUpdatesAsync ( streamingUpdates ,  openAIOptions ,  openAIConversationId ,   cancellationToken :  cancellationToken ) ; 
252254    } 
253255
254256    internal  static async  IAsyncEnumerable < ChatResponseUpdate >  FromOpenAIStreamingResponseUpdatesAsync ( 
255257        IAsyncEnumerable < StreamingResponseUpdate >  streamingResponseUpdates , 
256258        ResponseCreationOptions ?  options , 
259+         string ?  conversationId , 
257260        string ?  resumeResponseId  =  null , 
258261        [ EnumeratorCancellation ]  CancellationToken  cancellationToken  =  default ) 
259262    { 
260263        DateTimeOffset ?  createdAt  =  null ; 
261264        string ?  responseId  =  resumeResponseId ; 
262-         string ?  conversationId  =  options ? . StoredOutputEnabled  is  false  ?  null  :  resumeResponseId ; 
263265        string ?  modelId  =  null ; 
264266        string ?  lastMessageId  =  null ; 
265267        ChatRole ?  lastRole  =  null ; 
266268        bool  anyFunctions  =  false ; 
267269        ResponseStatus ?  latestResponseStatus  =  null ; 
268270
271+         UpdateConversationId ( resumeResponseId ) ; 
272+ 
269273        await  foreach  ( var  streamingUpdate  in  streamingResponseUpdates . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ) 
270274        { 
271275            // Create an update populated with the current state of the response. 
@@ -290,39 +294,39 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
290294                case  StreamingResponseCreatedUpdate  createdUpdate : 
291295                    createdAt  =  createdUpdate . Response . CreatedAt ; 
292296                    responseId  =  createdUpdate . Response . Id ; 
293-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
297+                     UpdateConversationId ( responseId ) ; 
294298                    modelId  =  createdUpdate . Response . Model ; 
295299                    latestResponseStatus  =  createdUpdate . Response . Status ; 
296300                    goto  default ; 
297301
298302                case  StreamingResponseQueuedUpdate  queuedUpdate : 
299303                    createdAt  =  queuedUpdate . Response . CreatedAt ; 
300304                    responseId  =  queuedUpdate . Response . Id ; 
301-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
305+                     UpdateConversationId ( responseId ) ; 
302306                    modelId  =  queuedUpdate . Response . Model ; 
303307                    latestResponseStatus  =  queuedUpdate . Response . Status ; 
304308                    goto  default ; 
305309
306310                case  StreamingResponseInProgressUpdate  inProgressUpdate : 
307311                    createdAt  =  inProgressUpdate . Response . CreatedAt ; 
308312                    responseId  =  inProgressUpdate . Response . Id ; 
309-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
313+                     UpdateConversationId ( responseId ) ; 
310314                    modelId  =  inProgressUpdate . Response . Model ; 
311315                    latestResponseStatus  =  inProgressUpdate . Response . Status ; 
312316                    goto  default ; 
313317
314318                case  StreamingResponseIncompleteUpdate  incompleteUpdate : 
315319                    createdAt  =  incompleteUpdate . Response . CreatedAt ; 
316320                    responseId  =  incompleteUpdate . Response . Id ; 
317-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
321+                     UpdateConversationId ( responseId ) ; 
318322                    modelId  =  incompleteUpdate . Response . Model ; 
319323                    latestResponseStatus  =  incompleteUpdate . Response . Status ; 
320324                    goto  default ; 
321325
322326                case  StreamingResponseFailedUpdate  failedUpdate : 
323327                    createdAt  =  failedUpdate . Response . CreatedAt ; 
324328                    responseId  =  failedUpdate . Response . Id ; 
325-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
329+                     UpdateConversationId ( responseId ) ; 
326330                    modelId  =  failedUpdate . Response . Model ; 
327331                    latestResponseStatus  =  failedUpdate . Response . Status ; 
328332                    goto  default ; 
@@ -331,7 +335,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
331335                { 
332336                    createdAt  =  completedUpdate . Response . CreatedAt ; 
333337                    responseId  =  completedUpdate . Response . Id ; 
334-                     conversationId   =   options ? . StoredOutputEnabled   is   false   ?   null   :   responseId ; 
338+                     UpdateConversationId ( responseId ) ; 
335339                    modelId  =  completedUpdate . Response . Model ; 
336340                    latestResponseStatus  =  completedUpdate . Response ? . Status ; 
337341                    var  update  =  CreateUpdate ( ToUsageDetails ( completedUpdate . Response )  is  {  }  usage  ?  new  UsageContent ( usage )  :  null ) ; 
@@ -434,6 +438,18 @@ outputItemDoneUpdate.Item is MessageResponseItem mri &&
434438                    break ; 
435439            } 
436440        } 
441+ 
442+         void  UpdateConversationId ( string ?  id ) 
443+         { 
444+             if  ( options ? . StoredOutputEnabled  is  false ) 
445+             { 
446+                 conversationId  =  null ; 
447+             } 
448+             else 
449+             { 
450+                 conversationId  ??=  id ; 
451+             } 
452+         } 
437453    } 
438454
439455    /// <inheritdoc /> 
@@ -563,25 +579,100 @@ private static ChatRole ToChatRole(MessageRole? role) =>
563579        null ; 
564580
565581    /// <summary>Converts a <see cref="ChatOptions"/> to a <see cref="ResponseCreationOptions"/>.</summary> 
566-     private  ResponseCreationOptions  ToOpenAIResponseCreationOptions ( ChatOptions ?  options ) 
582+     private  ResponseCreationOptions  ToOpenAIResponseCreationOptions ( ChatOptions ?  options ,   out   string ?   openAIConversationId ) 
567583    { 
584+         openAIConversationId  =  null ; 
585+ 
568586        if  ( options  is  null ) 
569587        { 
570588            return  new  ResponseCreationOptions ( ) ; 
571589        } 
572590
573-         if  ( options . RawRepresentationFactory ? . Invoke ( this )  is  not ResponseCreationOptions  result ) 
591+         bool  hasRawRco  =  false ; 
592+         if  ( options . RawRepresentationFactory ? . Invoke ( this )  is  ResponseCreationOptions  result ) 
593+         { 
594+             hasRawRco  =  true ; 
595+         } 
596+         else 
574597        { 
575598            result  =  new  ResponseCreationOptions ( ) ; 
576599        } 
577600
578-         // Handle strongly-typed properties. 
579601        result . MaxOutputTokenCount  ??=  options . MaxOutputTokens ; 
580-         result . PreviousResponseId  ??=  options . ConversationId ; 
581602        result . Temperature  ??=  options . Temperature ; 
582603        result . TopP  ??=  options . TopP ; 
583604        result . BackgroundModeEnabled  ??=  options . AllowBackgroundResponses ; 
584605
606+         // If the ResponseCreationOptions.PreviousResponseId is already set (likely rare), then we don't need to do 
607+         // anything with regards to Conversation, because they're mutually exclusive and we would want to ignore 
608+         // ChatOptions.ConversationId regardless of its value. If it's null, we want to examine the ResponseCreationOptions 
609+         // instance to see if a conversation ID has already been set on it and use that conversation ID subsequently if 
610+         // it has. If one hasn't been set, but ChatOptions.ConversationId has been set, we'll either set 
611+         // ResponseCreationOptions.Conversation if the string represents a conversation ID or else PreviousResponseId. 
612+         if  ( result . PreviousResponseId  is  null ) 
613+         { 
614+             // Technically, OpenAI's IDs are opaque. However, by convention conversation IDs start with "conv_" and 
615+             // we can use that to disambiguate whether we're looking at a conversation ID or a response ID. 
616+             string ?  chatOptionsConversationId  =  options . ConversationId ; 
617+             bool  chatOptionsHasOpenAIConversationId  =  chatOptionsConversationId ? . StartsWith ( "conv_" ,  StringComparison . OrdinalIgnoreCase )  is  true ; 
618+ 
619+             if  ( hasRawRco  ||  chatOptionsHasOpenAIConversationId ) 
620+             { 
621+                 const  string  ConversationPropertyName  =  "conversation" ; 
622+                 try 
623+                 { 
624+                     // ResponseCreationOptions currently doesn't expose either Conversation nor JSON Path for accessing 
625+                     // arbitrary properties publicly. Until it does, we need to serialize the RCO and examine 
626+                     // and possibly mutate/deserialize the resulting JSON. 
627+                     var  rcoJsonModel  =  ( IJsonModel < ResponseCreationOptions > ) result ; 
628+                     var  rcoJsonBinaryData  =  rcoJsonModel . Write ( ModelReaderWriterOptions . Json ) ; 
629+                     if  ( JsonNode . Parse ( rcoJsonBinaryData . ToMemory ( ) . Span )  is  JsonObject  rcoJsonObject ) 
630+                     { 
631+                         // Check if a conversation ID is already set on the RCO. If one is, store it for later. 
632+                         if  ( rcoJsonObject . TryGetPropertyValue ( ConversationPropertyName ,  out  JsonNode ?  existingConversationNode ) ) 
633+                         { 
634+                             switch  ( existingConversationNode ? . GetValueKind ( ) ) 
635+                             { 
636+                                 case  JsonValueKind . String : 
637+                                     openAIConversationId  =  existingConversationNode . GetValue < string > ( ) ; 
638+                                     break ; 
639+ 
640+                                 case  JsonValueKind . Object : 
641+                                     openAIConversationId  = 
642+                                         existingConversationNode . AsObject ( ) . TryGetPropertyValue ( "id" ,  out  JsonNode ?  idNode )  &&  idNode ? . GetValueKind ( )  ==  JsonValueKind . String  ? 
643+                                             idNode . GetValue < string > ( )  : 
644+                                             null ; 
645+                                     break ; 
646+                             } 
647+                         } 
648+ 
649+                         // If one isn't set, and ChatOptions.ConversationId is set to a conversation ID, set it now. 
650+                         if  ( openAIConversationId  is  null  &&  chatOptionsHasOpenAIConversationId ) 
651+                         { 
652+                             rcoJsonObject [ ConversationPropertyName ]  =  JsonValue . Create ( chatOptionsConversationId ) ; 
653+                             rcoJsonBinaryData  =  new ( JsonSerializer . SerializeToUtf8Bytes ( rcoJsonObject ,  AIJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( JsonNode ) ) ) ) ; 
654+                             if  ( rcoJsonModel . Create ( rcoJsonBinaryData ,  ModelReaderWriterOptions . Json )  is  ResponseCreationOptions  newRco ) 
655+                             { 
656+                                 result  =  newRco ; 
657+                                 openAIConversationId  =  chatOptionsConversationId ; 
658+                             } 
659+                         } 
660+                     } 
661+                 } 
662+                 catch 
663+                 { 
664+                     // Ignore any JSON formatting / parsing failures 
665+                 } 
666+             } 
667+ 
668+             // If after all that we still don't have a conversation ID, and ChatOptions.ConversationId is set, 
669+             // treat it as a response ID. 
670+             if  ( openAIConversationId  is  null  &&  options . ConversationId  is  {  }  previousResponseId ) 
671+             { 
672+                 result . PreviousResponseId  =  previousResponseId ; 
673+             } 
674+         } 
675+ 
585676        if  ( options . Instructions  is  {  }  instructions ) 
586677        { 
587678            result . Instructions  =  string . IsNullOrEmpty ( result . Instructions )  ? 
0 commit comments