11// Copyright (c) Microsoft. All rights reserved.
22
3- using System . Buffers ;
4- using System . ClientModel . Primitives ;
3+ using System ;
54using System . Collections . Generic ;
6- using System . Diagnostics ;
5+ using System . Linq ;
76using System . Net . ServerSentEvents ;
87using System . Runtime . CompilerServices ;
98using System . Text . Json ;
109using System . Threading ;
1110using System . Threading . Tasks ;
12- using Microsoft . Agents . AI . Hosting . OpenAI . ChatCompletions . Utils ;
11+ using Microsoft . Agents . AI . Hosting . OpenAI . ChatCompletions . Converters ;
12+ using Microsoft . Agents . AI . Hosting . OpenAI . ChatCompletions . Models ;
1313using Microsoft . AspNetCore . Http ;
1414using Microsoft . AspNetCore . Http . Features ;
15- using OpenAI . Chat ;
16- using ChatMessage = Microsoft . Extensions . AI . ChatMessage ;
15+ using Microsoft . Extensions . AI ;
1716
1817namespace Microsoft . Agents . AI . Hosting . OpenAI . ChatCompletions ;
1918
20- internal sealed class AIAgentChatCompletionsProcessor
19+ internal static class AIAgentChatCompletionsProcessor
2120{
22- private readonly AIAgent _agent ;
23-
24- public AIAgentChatCompletionsProcessor ( AIAgent agent )
21+ public static async Task < IResult > CreateChatCompletionAsync ( AIAgent agent , CreateChatCompletion request , CancellationToken cancellationToken )
2522 {
26- this . _agent = agent ;
27- }
23+ ArgumentNullException . ThrowIfNull ( agent ) ;
2824
29- public async Task < IResult > CreateChatCompletionAsync ( ChatCompletionOptions chatCompletionOptions , CancellationToken cancellationToken )
30- {
31- AgentThread ? agentThread = null ; // not supported to resolve from conversationId
25+ var chatMessages = request . Messages . Select ( i => i . ToChatMessage ( ) ) ;
26+ var chatClientAgentRunOptions = request . BuildOptions ( ) ;
3227
33- var inputItems = chatCompletionOptions . GetMessages ( ) ;
34- var chatMessages = inputItems . AsChatMessages ( ) ;
35-
36- if ( chatCompletionOptions . GetStream ( ) )
28+ if ( request . Stream == true )
3729 {
38- return new OpenAIStreamingChatCompletionResult ( this . _agent , chatMessages ) ;
30+ return new StreamingResponse ( agent , request , chatMessages , chatClientAgentRunOptions ) ;
3931 }
4032
41- var agentResponse = await this . _agent . RunAsync ( chatMessages , agentThread , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
42- return new OpenAIChatCompletionResult ( agentResponse ) ;
33+ var response = await agent . RunAsync ( chatMessages , options : chatClientAgentRunOptions , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
34+ return Results . Ok ( response . ToChatCompletion ( request ) ) ;
4335 }
4436
45- private sealed class OpenAIChatCompletionResult ( AgentRunResponse agentRunResponse ) : IResult
46- {
47- public async Task ExecuteAsync ( HttpContext httpContext )
48- {
49- // note: OpenAI SDK types provide their own serialization implementation
50- // so we cant simply return IResult wrap for the typed-object.
51- // instead writing to the response body can be done.
52-
53- var cancellationToken = httpContext . RequestAborted ;
54- var response = httpContext . Response ;
55-
56- var chatResponse = agentRunResponse . AsChatResponse ( ) ;
57- var openAIChatCompletion = chatResponse . AsOpenAIChatCompletion ( ) ;
58- var openAIChatCompletionJsonModel = openAIChatCompletion as IJsonModel < ChatCompletion > ;
59- Debug . Assert ( openAIChatCompletionJsonModel is not null ) ;
60-
61- var writer = new Utf8JsonWriter ( response . BodyWriter , new JsonWriterOptions { SkipValidation = false } ) ;
62- openAIChatCompletionJsonModel . Write ( writer , ModelReaderWriterOptions . Json ) ;
63- await writer . FlushAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
64- }
65- }
66-
67- private sealed class OpenAIStreamingChatCompletionResult ( AIAgent agent , IEnumerable < ChatMessage > chatMessages ) : IResult
37+ private sealed class StreamingResponse (
38+ AIAgent agent ,
39+ CreateChatCompletion request ,
40+ IEnumerable < ChatMessage > chatMessages ,
41+ ChatClientAgentRunOptions ? options ) : IResult
6842 {
6943 public Task ExecuteAsync ( HttpContext httpContext )
7044 {
@@ -79,26 +53,99 @@ public Task ExecuteAsync(HttpContext httpContext)
7953 httpContext . Features . GetRequiredFeature < IHttpResponseBodyFeature > ( ) . DisableBuffering ( ) ;
8054
8155 return SseFormatter . WriteAsync (
82- source : this . GetStreamingResponsesAsync ( cancellationToken ) ,
56+ source : this . GetStreamingChunksAsync ( cancellationToken ) ,
8357 destination : response . Body ,
8458 itemFormatter : ( sseItem , bufferWriter ) =>
8559 {
86- var sseDataJsonModel = ( IJsonModel < StreamingChatCompletionUpdate > ) sseItem . Data ;
87- var json = sseDataJsonModel . Write ( ModelReaderWriterOptions . Json ) ;
88- bufferWriter . Write ( json ) ;
60+ using var writer = new Utf8JsonWriter ( bufferWriter ) ;
61+ JsonSerializer . Serialize ( writer , sseItem . Data , ChatCompletionsJsonContext . Default . ChatCompletionChunk ) ;
62+ writer . Flush ( ) ;
8963 } ,
9064 cancellationToken ) ;
9165 }
9266
93- private async IAsyncEnumerable < SseItem < StreamingChatCompletionUpdate > > GetStreamingResponsesAsync ( [ EnumeratorCancellation ] CancellationToken cancellationToken = default )
67+ private async IAsyncEnumerable < SseItem < ChatCompletionChunk > > GetStreamingChunksAsync ( [ EnumeratorCancellation ] CancellationToken cancellationToken = default )
9468 {
95- AgentThread ? agentThread = null ;
69+ // The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
70+ DateTimeOffset ? createdAt = null ;
71+ var chunkId = IdGeneratorHelpers . NewId ( prefix : "chatcmpl" , delimiter : "-" , stringLength : 13 ) ;
9672
97- var agentRunResponseUpdates = agent . RunStreamingAsync ( chatMessages , thread : agentThread , cancellationToken : cancellationToken ) ;
98- var chatResponseUpdates = agentRunResponseUpdates . AsChatResponseUpdatesAsync ( ) ;
99- await foreach ( var streamingChatCompletionUpdate in chatResponseUpdates . AsOpenAIStreamingChatCompletionUpdatesAsync ( cancellationToken ) . ConfigureAwait ( false ) )
73+ await foreach ( var agentRunResponseUpdate in agent . RunStreamingAsync ( chatMessages , options : options , cancellationToken : cancellationToken ) . WithCancellation ( cancellationToken ) )
10074 {
101- yield return new SseItem < StreamingChatCompletionUpdate > ( streamingChatCompletionUpdate ) ;
75+ var finishReason = ( agentRunResponseUpdate . RawRepresentation is ChatResponseUpdate { FinishReason : not null } chatResponseUpdate )
76+ ? chatResponseUpdate . FinishReason . ToString ( )
77+ : "stop" ;
78+
79+ var choiceChunks = new List < ChatCompletionChoiceChunk > ( ) ;
80+ CompletionUsage ? usageDetails = null ;
81+
82+ createdAt ??= agentRunResponseUpdate . CreatedAt ;
83+
84+ foreach ( var content in agentRunResponseUpdate . Contents )
85+ {
86+ // usage content is handled separately
87+ if ( content is UsageContent usageContent && usageContent . Details != null )
88+ {
89+ usageDetails = usageContent . Details . ToCompletionUsage ( ) ;
90+ continue ;
91+ }
92+
93+ ChatCompletionDelta ? delta = content switch
94+ {
95+ TextContent textContent => new ( ) { Content = textContent . Text } ,
96+
97+ // image
98+ DataContent imageContent when imageContent . HasTopLevelMediaType ( "image" ) => new ( ) { Content = imageContent . Base64Data . ToString ( ) } ,
99+ UriContent urlContent when urlContent . HasTopLevelMediaType ( "image" ) => new ( ) { Content = urlContent . Uri . ToString ( ) } ,
100+
101+ // audio
102+ DataContent audioContent when audioContent . HasTopLevelMediaType ( "audio" ) => new ( ) { Content = audioContent . Base64Data . ToString ( ) } ,
103+
104+ // file
105+ DataContent fileContent => new ( ) { Content = fileContent . Base64Data . ToString ( ) } ,
106+ HostedFileContent fileContent => new ( ) { Content = fileContent . FileId } ,
107+
108+ // function call
109+ FunctionCallContent functionCallContent => new ( )
110+ {
111+ ToolCalls = [ functionCallContent . ToChoiceMessageToolCall ( ) ]
112+ } ,
113+
114+ // function result. ChatCompletions dont provide the results of function result per API reference
115+ FunctionResultContent functionResultContent => null ,
116+
117+ // ignore
118+ _ => null
119+ } ;
120+
121+ if ( delta is null )
122+ {
123+ // unsupported but expected content type.
124+ continue ;
125+ }
126+
127+ delta . Role = agentRunResponseUpdate . Role ? . Value ?? "user" ;
128+
129+ var choiceChunk = new ChatCompletionChoiceChunk
130+ {
131+ Index = 0 ,
132+ Delta = delta ,
133+ FinishReason = finishReason
134+ } ;
135+
136+ choiceChunks . Add ( choiceChunk ) ;
137+ }
138+
139+ var chunk = new ChatCompletionChunk
140+ {
141+ Id = chunkId ,
142+ Created = ( createdAt ?? DateTimeOffset . UtcNow ) . ToUnixTimeSeconds ( ) ,
143+ Model = request . Model ,
144+ Choices = choiceChunks ,
145+ Usage = usageDetails
146+ } ;
147+
148+ yield return new ( chunk ) ;
102149 }
103150 }
104151 }
0 commit comments