Skip to content

Commit 6422310

Browse files
stephentoubjeffhandley
authored andcommitted
Update to OpenAI 2.7.0 (dotnet#7044)
1 parent 492a83c commit 6422310

3 files changed

Lines changed: 138 additions & 29 deletions

File tree

eng/packages/General.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageVersion Include="ModelContextProtocol.Core" Version="0.4.0-preview.3" />
2222
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
2323
<PackageVersion Include="OllamaSharp" Version="5.1.9" />
24-
<PackageVersion Include="OpenAI" Version="2.6.0" />
24+
<PackageVersion Include="OpenAI" Version="2.7.0" />
2525
<PackageVersion Include="Polly" Version="8.4.2" />
2626
<PackageVersion Include="Polly.Core" Version="8.4.2" />
2727
<PackageVersion Include="Polly.Extensions" Version="8.4.2" />

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.ClientModel;
66
using System.ClientModel.Primitives;
77
using System.Collections.Generic;
8-
using System.Diagnostics.CodeAnalysis;
98
using System.Linq;
109
using System.Reflection;
1110
using System.Runtime.CompilerServices;
@@ -27,11 +26,6 @@ namespace Microsoft.Extensions.AI;
2726
/// <summary>Represents an <see cref="IChatClient"/> for an <see cref="OpenAIResponseClient"/>.</summary>
2827
internal sealed class OpenAIResponsesChatClient : IChatClient
2928
{
30-
// Fix this to not use reflection once https://github.com/openai/openai-dotnet/issues/643 is addressed.
31-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
32-
private static readonly Type? _internalResponseReasoningSummaryTextDeltaEventType = Type.GetType("OpenAI.Responses.InternalResponseReasoningSummaryTextDeltaEvent, OpenAI");
33-
private static readonly PropertyInfo? _summaryTextDeltaProperty = _internalResponseReasoningSummaryTextDeltaEventType?.GetProperty("Delta");
34-
3529
// These delegate instances are used to call the internal overloads of CreateResponseAsync and CreateResponseStreamingAsync that accept
3630
// a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
3731
private static readonly Func<OpenAIResponseClient, IEnumerable<ResponseItem>, ResponseCreationOptions, RequestOptions, Task<ClientResult<OpenAIResponse>>>?
@@ -393,6 +387,14 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
393387
yield return CreateUpdate(new TextContent(outputTextDeltaUpdate.Delta));
394388
break;
395389

390+
case StreamingResponseReasoningSummaryTextDeltaUpdate reasoningSummaryTextDeltaUpdate:
391+
yield return CreateUpdate(new TextReasoningContent(reasoningSummaryTextDeltaUpdate.Delta));
392+
break;
393+
394+
case StreamingResponseReasoningTextDeltaUpdate reasoningTextDeltaUpdate:
395+
yield return CreateUpdate(new TextReasoningContent(reasoningTextDeltaUpdate.Delta));
396+
break;
397+
396398
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is FunctionCallResponseItem fcri:
397399
yield return CreateUpdate(OpenAIClientExtensions.ParseCallContent(fcri.FunctionArguments.ToString(), fcri.CallId, fcri.FunctionName));
398400
break;
@@ -452,19 +454,11 @@ outputItemDoneUpdate.Item is MessageResponseItem mri &&
452454
});
453455
break;
454456

455-
// Replace with public StreamingResponseReasoningSummaryTextDelta when available
456-
case StreamingResponseUpdate when
457-
streamingUpdate.GetType() == _internalResponseReasoningSummaryTextDeltaEventType &&
458-
_summaryTextDeltaProperty?.GetValue(streamingUpdate) is string delta:
459-
yield return CreateUpdate(new TextReasoningContent(delta));
460-
break;
461-
462457
case StreamingResponseImageGenerationCallInProgressUpdate imageGenInProgress:
463458
yield return CreateUpdate(new ImageGenerationToolCallContent
464459
{
465460
ImageId = imageGenInProgress.ItemId,
466461
RawRepresentation = imageGenInProgress,
467-
468462
});
469463
goto default;
470464

@@ -1203,6 +1197,7 @@ private static void PopulateAnnotations(ResponseContentPart source, AIContent de
12031197

12041198
case FileCitationMessageAnnotation fcma:
12051199
ca.FileId = fcma.FileId;
1200+
ca.Title = fcma.Filename;
12061201
break;
12071202

12081203
case UriCitationMessageAnnotation ucma:
@@ -1300,26 +1295,13 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
13001295
var imageGenTool = options?.Tools.OfType<ImageGenerationTool>().FirstOrDefault();
13011296
var outputType = imageGenTool?.OutputFileFormat?.ToString() ?? "png";
13021297

1303-
var bytes = update.PartialImageBytes;
1304-
1305-
if (bytes is null || bytes.Length == 0)
1306-
{
1307-
// workaround https://github.com/openai/openai-dotnet/issues/809
1308-
if (update.Patch.TryGetJson("$.partial_image_b64"u8, out var jsonBytes))
1309-
{
1310-
Utf8JsonReader reader = new(jsonBytes.Span);
1311-
_ = reader.Read();
1312-
bytes = BinaryData.FromBytes(reader.GetBytesFromBase64());
1313-
}
1314-
}
1315-
13161298
return new ImageGenerationToolResultContent
13171299
{
13181300
ImageId = update.ItemId,
13191301
RawRepresentation = update,
13201302
Outputs = new List<AIContent>
13211303
{
1322-
new DataContent(bytes, $"image/{outputType}")
1304+
new DataContent(update.PartialImageBytes, $"image/{outputType}")
13231305
{
13241306
AdditionalProperties = new()
13251307
{

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,133 @@ public async Task BasicReasoningResponse_Streaming()
351351
Assert.Equal(139, usage.Details.TotalTokenCount);
352352
}
353353

354+
[Fact]
355+
public async Task ReasoningTextDelta_Streaming()
356+
{
357+
const string Input = """
358+
{
359+
"input":[{
360+
"type":"message",
361+
"role":"user",
362+
"content":[{
363+
"type":"input_text",
364+
"text":"Solve this problem step by step."
365+
}]
366+
}],
367+
"reasoning": {
368+
"effort": "medium"
369+
},
370+
"model": "o4-mini",
371+
"stream": true
372+
}
373+
""";
374+
375+
const string Output = """
376+
event: response.created
377+
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_reasoning123","object":"response","created_at":1756752900,"status":"in_progress","model":"o4-mini-2025-04-16","output":[],"reasoning":{"effort":"medium"}}}
378+
379+
event: response.in_progress
380+
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_reasoning123","object":"response","created_at":1756752900,"status":"in_progress","model":"o4-mini-2025-04-16","output":[]}}
381+
382+
event: response.output_item.added
383+
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"rs_reasoning123","type":"reasoning","text":""}}
384+
385+
event: response.reasoning_text.delta
386+
data: {"type":"response.reasoning_text.delta","sequence_number":3,"item_id":"rs_reasoning123","output_index":0,"delta":"First, "}
387+
388+
event: response.reasoning_text.delta
389+
data: {"type":"response.reasoning_text.delta","sequence_number":4,"item_id":"rs_reasoning123","output_index":0,"delta":"let's analyze "}
390+
391+
event: response.reasoning_text.delta
392+
data: {"type":"response.reasoning_text.delta","sequence_number":5,"item_id":"rs_reasoning123","output_index":0,"delta":"the problem."}
393+
394+
event: response.reasoning_text.done
395+
data: {"type":"response.reasoning_text.done","sequence_number":6,"item_id":"rs_reasoning123","output_index":0,"text":"First, let's analyze the problem."}
396+
397+
event: response.output_item.done
398+
data: {"type":"response.output_item.done","sequence_number":7,"output_index":0,"item":{"id":"rs_reasoning123","type":"reasoning","text":"First, let's analyze the problem."}}
399+
400+
event: response.output_item.added
401+
data: {"type":"response.output_item.added","sequence_number":8,"output_index":1,"item":{"id":"msg_reasoning123","type":"message","status":"in_progress","content":[],"role":"assistant"}}
402+
403+
event: response.content_part.added
404+
data: {"type":"response.content_part.added","sequence_number":9,"item_id":"msg_reasoning123","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"text":""}}
405+
406+
event: response.output_text.delta
407+
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_reasoning123","output_index":1,"content_index":0,"delta":"The solution is 42."}
408+
409+
event: response.output_text.done
410+
data: {"type":"response.output_text.done","sequence_number":11,"item_id":"msg_reasoning123","output_index":1,"content_index":0,"text":"The solution is 42."}
411+
412+
event: response.content_part.done
413+
data: {"type":"response.content_part.done","sequence_number":12,"item_id":"msg_reasoning123","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"text":"The solution is 42."}}
414+
415+
event: response.output_item.done
416+
data: {"type":"response.output_item.done","sequence_number":13,"output_index":1,"item":{"id":"msg_reasoning123","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"The solution is 42."}],"role":"assistant"}}
417+
418+
event: response.completed
419+
data: {"type":"response.completed","sequence_number":14,"response":{"id":"resp_reasoning123","object":"response","created_at":1756752900,"status":"completed","model":"o4-mini-2025-04-16","output":[{"id":"rs_reasoning123","type":"reasoning","text":"First, let's analyze the problem."},{"id":"msg_reasoning123","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"The solution is 42."}],"role":"assistant"}],"usage":{"input_tokens":10,"output_tokens":25,"total_tokens":35}}}
420+
421+
422+
""";
423+
424+
using VerbatimHttpHandler handler = new(Input, Output);
425+
using HttpClient httpClient = new(handler);
426+
using IChatClient client = CreateResponseClient(httpClient, "o4-mini");
427+
428+
List<ChatResponseUpdate> updates = [];
429+
await foreach (var update in client.GetStreamingResponseAsync("Solve this problem step by step.", new()
430+
{
431+
RawRepresentationFactory = options => new ResponseCreationOptions
432+
{
433+
ReasoningOptions = new()
434+
{
435+
ReasoningEffortLevel = ResponseReasoningEffortLevel.Medium
436+
}
437+
}
438+
}))
439+
{
440+
updates.Add(update);
441+
}
442+
443+
Assert.Equal("The solution is 42.", string.Concat(updates.Where(u => u.Role == ChatRole.Assistant).Select(u => u.Text)));
444+
445+
var createdAt = DateTimeOffset.FromUnixTimeSeconds(1_756_752_900);
446+
Assert.Equal(15, updates.Count);
447+
448+
for (int i = 0; i < updates.Count; i++)
449+
{
450+
Assert.Equal("resp_reasoning123", updates[i].ResponseId);
451+
Assert.Equal(createdAt, updates[i].CreatedAt);
452+
Assert.Equal("o4-mini-2025-04-16", updates[i].ModelId);
453+
}
454+
455+
// Verify reasoning text delta updates (sequence 3-5)
456+
var reasoningUpdates = updates.Where((u, idx) => idx >= 3 && idx <= 5).ToList();
457+
Assert.Equal(3, reasoningUpdates.Count);
458+
Assert.All(reasoningUpdates, u =>
459+
{
460+
Assert.Single(u.Contents);
461+
Assert.Null(u.Role);
462+
var reasoning = Assert.IsType<TextReasoningContent>(u.Contents.Single());
463+
Assert.NotNull(reasoning.Text);
464+
});
465+
466+
// Verify the reasoning text content
467+
var allReasoningText = string.Concat(reasoningUpdates.Select(u => u.Contents.OfType<TextReasoningContent>().First().Text));
468+
Assert.Equal("First, let's analyze the problem.", allReasoningText);
469+
470+
// Verify assistant response
471+
var assistantUpdate = updates.First(u => u.Role == ChatRole.Assistant && !string.IsNullOrEmpty(u.Text));
472+
Assert.Equal("The solution is 42.", assistantUpdate.Text);
473+
474+
// Verify usage
475+
UsageContent usage = updates.SelectMany(u => u.Contents).OfType<UsageContent>().Single();
476+
Assert.Equal(10, usage.Details.InputTokenCount);
477+
Assert.Equal(25, usage.Details.OutputTokenCount);
478+
Assert.Equal(35, usage.Details.TotalTokenCount);
479+
}
480+
354481
[Fact]
355482
public async Task BasicRequestResponse_Streaming()
356483
{

0 commit comments

Comments
 (0)