diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 7ff9332fbec0..b58af11e54b2 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -10,7 +10,7 @@
-
+
@@ -49,7 +49,7 @@
-
+
diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_Function.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_Function.cs
index 017c2d3c9877..cda940eb82d6 100644
--- a/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_Function.cs
+++ b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step05_AssistantTool_Function.cs
@@ -29,9 +29,13 @@ public async Task UseSingleAssistantWithFunctionToolsAsync()
{
Name = HostName,
Instructions = HostInstructions,
- Metadata = AssistantSampleMetadata,
};
+ foreach (var kv in AssistantSampleMetadata)
+ {
+ creationOptions.Metadata.Add(kv.Key, kv.Value);
+ }
+
// In this sample the function tools are added to the assistant this is
// important if you want to retrieve the assistant later and then dynamically check
// what function tools it requires.
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
index 9d2b01cda30f..b8a0a3778745 100644
--- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
@@ -256,7 +256,7 @@ await functionProcessor.InvokeFunctionCallsAsync(
int messageCount = 0;
foreach (RunStep completedStep in completedStepsToProcess)
{
- if (completedStep.Type == RunStepType.ToolCalls)
+ if (completedStep.Kind == RunStepKind.ToolCall)
{
foreach (RunStepToolCall toolCall in completedStep.Details.ToolCalls)
{
@@ -264,15 +264,15 @@ await functionProcessor.InvokeFunctionCallsAsync(
ChatMessageContent? content = null;
// Process code-interpreter content
- if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter)
+ if (toolCall.Kind == RunStepToolCallKind.CodeInterpreter)
{
content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput, completedStep);
isVisible = true;
}
// Process function result content
- else if (toolCall.ToolKind == RunStepToolCallKind.Function)
+ else if (toolCall.Kind == RunStepToolCallKind.Function)
{
- FunctionResultContent functionStep = functionSteps[toolCall.ToolCallId]; // Function step always captured on invocation
+ FunctionResultContent functionStep = functionSteps[toolCall.Id]; // Function step always captured on invocation
content = GenerateFunctionResultContent(agent.GetName(), [functionStep], completedStep);
}
@@ -284,7 +284,7 @@ await functionProcessor.InvokeFunctionCallsAsync(
}
}
}
- else if (completedStep.Type == RunStepType.MessageCreation)
+ else if (completedStep.Kind == RunStepKind.CreatedMessage)
{
// Retrieve the message
ThreadMessage? message = await RetrieveMessageAsync(client, threadId, completedStep.Details.CreatedMessageId, agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false);
@@ -506,7 +506,7 @@ await client.GetRunStepsAsync(run.ThreadId, run.Id, cancellationToken: cancellat
{
foreach (RunStepToolCall stepDetails in step.Details.ToolCalls)
{
- toolMap[stepDetails.ToolCallId] = step.Id;
+ toolMap[stepDetails.Id] = step.Id;
}
}
@@ -564,14 +564,14 @@ await RetrieveMessageAsync(
{
foreach (RunStepToolCall toolCall in step.Details.ToolCalls)
{
- if (toolCall.ToolKind == RunStepToolCallKind.Function)
+ if (toolCall.Kind == RunStepToolCallKind.Function)
{
messages?.Add(GenerateFunctionResultContent(agent.GetName(), stepFunctionResults[step.Id], step));
stepFunctionResults.Remove(step.Id);
break;
}
- if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter)
+ if (toolCall.Kind == RunStepToolCallKind.CodeInterpreter)
{
messages?.Add(GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput, step));
}
@@ -761,13 +761,13 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam
private static IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step)
{
- if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls)
+ if (step.Status == RunStepStatus.InProgress && step.Kind == RunStepKind.ToolCall)
{
foreach (RunStepToolCall toolCall in step.Details.ToolCalls)
{
(FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(toolCall.FunctionName, toolCall.FunctionArguments);
- FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments);
+ FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments);
yield return content;
}
diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureClientCoreTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureClientCoreTests.cs
new file mode 100644
index 000000000000..c9c47f07ee86
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureClientCoreTests.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using Azure.AI.OpenAI;
+using Azure.Core;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
+using Moq;
+
+namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core;
+
+public sealed class AzureClientCoreTests : IDisposable
+{
+ private readonly HttpClient _httpClient;
+ private readonly Mock _mockLogger;
+
+ public AzureClientCoreTests()
+ {
+ this._httpClient = new HttpClient();
+ this._mockLogger = new Mock();
+ }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ }
+
+ [Fact]
+ public void ConstructorWithValidParametersShouldInitializeCorrectly()
+ {
+ // Arrange
+ var deploymentName = "test-deployment";
+ var endpoint = "https://test-endpoint.openai.azure.com/";
+ var apiKey = "test-api-key";
+
+ // Act
+ var azureClientCore = new AzureClientCore(deploymentName, endpoint, apiKey, this._httpClient, this._mockLogger.Object);
+
+ // Assert
+ Assert.NotNull(azureClientCore.Client);
+ Assert.Equal(deploymentName, azureClientCore.DeploymentName);
+ Assert.Equal(new Uri(endpoint), azureClientCore.Endpoint);
+ }
+
+ [Fact]
+ public void ConstructorWithInvalidEndpointShouldThrowArgumentException()
+ {
+ // Arrange
+ var deploymentName = "test-deployment";
+ var invalidEndpoint = "http://invalid-endpoint";
+ var apiKey = "test-api-key";
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new AzureClientCore(deploymentName, invalidEndpoint, apiKey, this._httpClient, this._mockLogger.Object));
+ }
+
+ [Fact]
+ public void ConstructorWithTokenCredentialShouldInitializeCorrectly()
+ {
+ // Arrange
+ var deploymentName = "test-deployment";
+ var endpoint = "https://test-endpoint.openai.azure.com/";
+ var tokenCredential = new Mock().Object;
+
+ // Act
+ var azureClientCore = new AzureClientCore(deploymentName, endpoint, tokenCredential, this._httpClient, this._mockLogger.Object);
+
+ // Assert
+ Assert.NotNull(azureClientCore.Client);
+ Assert.Equal(deploymentName, azureClientCore.DeploymentName);
+ Assert.Equal(new Uri(endpoint), azureClientCore.Endpoint);
+ }
+
+ [Fact]
+ public void ConstructorWithOpenAIClientShouldInitializeCorrectly()
+ {
+ // Arrange
+ var deploymentName = "test-deployment";
+ var openAIClient = new Mock(MockBehavior.Strict, new Uri("https://test-endpoint.openai.azure.com/"), new Mock().Object).Object;
+
+ // Act
+ var azureClientCore = new AzureClientCore(deploymentName, openAIClient, this._mockLogger.Object);
+
+ // Assert
+ Assert.NotNull(azureClientCore.Client);
+ Assert.Equal(deploymentName, azureClientCore.DeploymentName);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs
index 336d12036db9..844501f729fa 100644
--- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs
+++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs
@@ -79,6 +79,17 @@ public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFacto
Assert.Equal("model-id", service.Attributes["ModelId"]);
}
+ [Theory]
+ [InlineData("invalid")]
+ public void ConstructorThrowsOnInvalidApiVersion(string? apiVersion)
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ _ = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", httpClient: this._httpClient, apiVersion: apiVersion);
+ });
+ }
+
[Theory]
[InlineData(true)]
[InlineData(false)]
@@ -122,8 +133,10 @@ public async Task GetTextContentsWorksCorrectlyAsync()
Assert.Equal(155, usage.TotalTokenCount);
}
- [Fact]
- public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync()
+ [Theory]
+ [InlineData("system")]
+ [InlineData("developer")]
+ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync(string historyRole)
{
// Arrange
var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient);
@@ -152,7 +165,14 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync()
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("User Message");
chatHistory.AddUserMessage([new ImageContent(new Uri("https://image")), new TextContent("User Message")]);
- chatHistory.AddSystemMessage("System Message");
+ if (historyRole == "system")
+ {
+ chatHistory.AddSystemMessage("System Message");
+ }
+ else
+ {
+ chatHistory.AddDeveloperMessage("Developer Message");
+ }
chatHistory.AddAssistantMessage("Assistant Message");
using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
@@ -189,8 +209,16 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync()
Assert.Equal("User Message", contentItems[1].GetProperty("text").GetString());
Assert.Equal("text", contentItems[1].GetProperty("type").GetString());
- Assert.Equal("system", systemMessage.GetProperty("role").GetString());
- Assert.Equal("System Message", systemMessage.GetProperty("content").GetString());
+ if (historyRole == "system")
+ {
+ Assert.Equal("system", systemMessage.GetProperty("role").GetString());
+ Assert.Equal("System Message", systemMessage.GetProperty("content").GetString());
+ }
+ else
+ {
+ Assert.Equal("developer", systemMessage.GetProperty("role").GetString());
+ Assert.Equal("Developer Message", systemMessage.GetProperty("content").GetString());
+ }
Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString());
Assert.Equal("Assistant Message", assistantMessage.GetProperty("content").GetString());
@@ -245,6 +273,66 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje
Assert.Equal(expectedResponseType, content.GetProperty("response_format").GetProperty("type").GetString());
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("string", "low")]
+ [InlineData("string", "medium")]
+ [InlineData("string", "high")]
+ [InlineData("ChatReasonEffortLevel.Low", "low")]
+ [InlineData("ChatReasonEffortLevel.Medium", "medium")]
+ [InlineData("ChatReasonEffortLevel.High", "high")]
+ public async Task GetChatMessageInReasoningEffortAsync(string? effortType, string? expectedEffortLevel)
+ {
+ // Assert
+ object? reasoningEffortObject = null;
+ switch (effortType)
+ {
+ case "string":
+ reasoningEffortObject = expectedEffortLevel;
+ break;
+ case "ChatReasonEffortLevel.Low":
+ reasoningEffortObject = ChatReasoningEffortLevel.Low;
+ break;
+ case "ChatReasonEffortLevel.Medium":
+ reasoningEffortObject = ChatReasoningEffortLevel.Medium;
+ break;
+ case "ChatReasonEffortLevel.High":
+ reasoningEffortObject = ChatReasoningEffortLevel.High;
+ break;
+ }
+
+ var modelId = "o1";
+ var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient);
+ OpenAIPromptExecutionSettings executionSettings = new() { ReasoningEffort = reasoningEffortObject };
+ using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json"))
+ };
+
+ this._messageHandlerStub.ResponsesToReturn.Add(responseMessage);
+
+ // Act
+ var result = await sut.GetChatMessageContentAsync(new ChatHistory("System message"), executionSettings);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!);
+ Assert.NotNull(actualRequestContent);
+
+ var optionsJson = JsonSerializer.Deserialize(actualRequestContent);
+
+ if (expectedEffortLevel is null)
+ {
+ Assert.False(optionsJson.TryGetProperty("reasoning_effort", out _));
+ return;
+ }
+
+ var requestedReasoningEffort = optionsJson.GetProperty("reasoning_effort").GetString();
+
+ Assert.Equal(expectedEffortLevel, requestedReasoningEffort);
+ }
+
[Theory]
[MemberData(nameof(ToolCallBehaviors))]
public async Task GetChatMessageContentsWorksCorrectlyAsync(ToolCallBehavior behavior)
@@ -806,6 +894,49 @@ public async Task GetChatMessageContentsUsesPromptAndSettingsCorrectlyAsync()
Assert.Equal("user", messages[1].GetProperty("role").GetString());
}
+ [Fact]
+ public async Task GetChatMessageContentsUsesDeveloperPromptAndSettingsCorrectlyAsync()
+ {
+ // Arrange
+ const string Prompt = "This is test prompt";
+ const string DeveloperMessage = "This is test system message";
+
+ var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient);
+ var settings = new AzureOpenAIPromptExecutionSettings() { ChatDeveloperPrompt = DeveloperMessage };
+
+ using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json"))
+ };
+ this._messageHandlerStub.ResponsesToReturn.Add(responseMessage);
+
+ IKernelBuilder builder = Kernel.CreateBuilder();
+ builder.Services.AddTransient((sp) => service);
+ Kernel kernel = builder.Build();
+
+ // Act
+ var result = await kernel.InvokePromptAsync(Prompt, new(settings));
+
+ // Assert
+ Assert.Equal("Test chat response", result.ToString());
+
+ var requestContentByteArray = this._messageHandlerStub.RequestContents[0];
+
+ Assert.NotNull(requestContentByteArray);
+
+ var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray));
+
+ var messages = requestContent.GetProperty("messages");
+
+ Assert.Equal(2, messages.GetArrayLength());
+
+ Assert.Equal(DeveloperMessage, messages[0].GetProperty("content").GetString());
+ Assert.Equal("developer", messages[0].GetProperty("role").GetString());
+
+ Assert.Equal(Prompt, messages[1].GetProperty("content").GetString());
+ Assert.Equal("user", messages[1].GetProperty("role").GetString());
+ }
+
[Fact]
public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync()
{
@@ -1537,6 +1668,14 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAndEmptyArgumen
public static TheoryData Versions => new()
{
+ { "V2025_01_01_preview", "2025-01-01-preview" },
+ { "V2025_01_01_PREVIEW", "2025-01-01-preview" },
+ { "2025_01_01_Preview", "2025-01-01-preview" },
+ { "2025-01-01-preview", "2025-01-01-preview" },
+ { "V2024_12_01_preview", "2024-12-01-preview" },
+ { "V2024_12_01_PREVIEW", "2024-12-01-preview" },
+ { "2024_12_01_Preview", "2024-12-01-preview" },
+ { "2024-12-01-preview", "2024-12-01-preview" },
{ "V2024_10_01_preview", "2024-10-01-preview" },
{ "V2024_10_01_PREVIEW", "2024-10-01-preview" },
{ "2024_10_01_Preview", "2024-10-01-preview" },
@@ -1552,10 +1691,16 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAndEmptyArgumen
{ "V2024_06_01", "2024-06-01" },
{ "2024_06_01", "2024-06-01" },
{ "2024-06-01", "2024-06-01" },
+ { "V2024_10_21", "2024-10-21" },
+ { "2024_10_21", "2024-10-21" },
+ { "2024-10-21", "2024-10-21" },
+ { AzureOpenAIClientOptions.ServiceVersion.V2025_01_01_Preview.ToString(), null },
+ { AzureOpenAIClientOptions.ServiceVersion.V2024_12_01_Preview.ToString(), null },
{ AzureOpenAIClientOptions.ServiceVersion.V2024_10_01_Preview.ToString(), null },
{ AzureOpenAIClientOptions.ServiceVersion.V2024_09_01_Preview.ToString(), null },
{ AzureOpenAIClientOptions.ServiceVersion.V2024_08_01_Preview.ToString(), null },
- { AzureOpenAIClientOptions.ServiceVersion.V2024_06_01.ToString(), null }
+ { AzureOpenAIClientOptions.ServiceVersion.V2024_06_01.ToString(), null },
+ { AzureOpenAIClientOptions.ServiceVersion.V2024_10_21.ToString(), null }
};
public void Dispose()
diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs
index 3ad42a32eac6..27e2b3ebc14d 100644
--- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs
+++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs
@@ -47,6 +47,23 @@ public void ConstructorsAddRequiredMetadata(bool includeLoggerFactory)
Assert.Equal("deployment-name", service.Attributes["DeploymentName"]);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConstructorTokenCredentialAddRequiredMetadata(bool includeLoggerFactory)
+ {
+ // Arrange & Act
+ var service = includeLoggerFactory ?
+ new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", Azure.Core.DelegatedTokenCredential.Create((context, ct)
+ => new Azure.Core.AccessToken("abc", DateTimeOffset.Now.AddMinutes(30))), "model-id", loggerFactory: this._mockLoggerFactory.Object) :
+ new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", Azure.Core.DelegatedTokenCredential.Create((context, ct)
+ => new Azure.Core.AccessToken("abc", DateTimeOffset.Now.AddMinutes(30))), "model-id");
+
+ // Assert
+ Assert.Equal("model-id", service.Attributes["ModelId"]);
+ Assert.Equal("deployment-name", service.Attributes["DeploymentName"]);
+ }
+
[Fact]
public void ItThrowsIfModelIdIsNotProvided()
{
diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs
index bf7859815f1d..687addf37087 100644
--- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs
+++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs
@@ -50,6 +50,7 @@ protected override ChatCompletionOptions CreateChatCompletionOptions(
TopLogProbabilityCount = executionSettings.TopLogprobs,
IncludeLogProbabilities = executionSettings.Logprobs,
StoredOutputEnabled = executionSettings.Store,
+ ReasoningEffortLevel = GetEffortLevel(executionSettings),
};
var responseFormat = GetResponseFormat(executionSettings);
diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs
index 5ad45701a921..a3dbbe730057 100644
--- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs
+++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs
@@ -135,9 +135,12 @@ internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient?
sdkVersion = serviceVersion.ToUpperInvariant() switch // Azure SDK versioning
{
"2024-06-01" or "V2024_06_01" or "2024_06_01" => AzureOpenAIClientOptions.ServiceVersion.V2024_06_01,
+ "2024-10-21" or "V2024_10_21" or "2024_10_21" => AzureOpenAIClientOptions.ServiceVersion.V2024_10_21,
"2024-08-01-PREVIEW" or "V2024_08_01_PREVIEW" or "2024_08_01_PREVIEW" => AzureOpenAIClientOptions.ServiceVersion.V2024_08_01_Preview,
"2024-09-01-PREVIEW" or "V2024_09_01_PREVIEW" or "2024_09_01_PREVIEW" => AzureOpenAIClientOptions.ServiceVersion.V2024_09_01_Preview,
"2024-10-01-PREVIEW" or "V2024_10_01_PREVIEW" or "2024_10_01_PREVIEW" => AzureOpenAIClientOptions.ServiceVersion.V2024_10_01_Preview,
+ "2024-12-01-PREVIEW" or "V2024_12_01_PREVIEW" or "2024_12_01_PREVIEW" => AzureOpenAIClientOptions.ServiceVersion.V2024_12_01_Preview,
+ "2025-01-01-PREVIEW" or "V2025_01_01_PREVIEW" or "2025_01_01_PREVIEW" => AzureOpenAIClientOptions.ServiceVersion.V2025_01_01_Preview,
_ => throw new NotSupportedException($"The service version '{serviceVersion}' is not supported.")
};
diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
index 74360e542358..d6b83f21a391 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs
@@ -589,6 +589,48 @@ public async Task GetChatMessageContentsUsesPromptAndSettingsCorrectlyAsync()
Assert.Equal("user", messages[1].GetProperty("role").GetString());
}
+ [Fact]
+ public async Task GetChatMessageContentsUsesDeveloperPromptAndSettingsCorrectlyAsync()
+ {
+ // Arrange
+ const string Prompt = "This is test prompt";
+ const string DeveloperMessage = "This is test system message";
+
+ var service = new OpenAIChatCompletionService("model-id", "api-key", httpClient: this._httpClient);
+ var settings = new OpenAIPromptExecutionSettings() { ChatDeveloperPrompt = DeveloperMessage };
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json"))
+ };
+
+ IKernelBuilder builder = Kernel.CreateBuilder();
+ builder.Services.AddTransient((sp) => service);
+ Kernel kernel = builder.Build();
+
+ // Act
+ var result = await kernel.InvokePromptAsync(Prompt, new(settings));
+
+ // Assert
+ Assert.Equal("Test chat response", result.ToString());
+
+ var requestContentByteArray = this._messageHandlerStub.RequestContent;
+
+ Assert.NotNull(requestContentByteArray);
+
+ var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray));
+
+ var messages = requestContent.GetProperty("messages");
+
+ Assert.Equal(2, messages.GetArrayLength());
+
+ Assert.Equal(DeveloperMessage, messages[0].GetProperty("content").GetString());
+ Assert.Equal("developer", messages[0].GetProperty("role").GetString());
+
+ Assert.Equal(Prompt, messages[1].GetProperty("content").GetString());
+ Assert.Equal("user", messages[1].GetProperty("role").GetString());
+ }
+
[Fact]
public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync()
{
@@ -962,6 +1004,65 @@ public async Task GetChatMessageInResponseFormatsAsync(string formatType, string
Assert.NotNull(result);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("string", "low")]
+ [InlineData("string", "medium")]
+ [InlineData("string", "high")]
+ [InlineData("ChatReasonEffortLevel.Low", "low")]
+ [InlineData("ChatReasonEffortLevel.Medium", "medium")]
+ [InlineData("ChatReasonEffortLevel.High", "high")]
+ public async Task GetChatMessageInReasoningEffortAsync(string? effortType, string? expectedEffortLevel)
+ {
+ // Assert
+ object? reasoningEffortObject = null;
+ switch (effortType)
+ {
+ case "string":
+ reasoningEffortObject = expectedEffortLevel;
+ break;
+ case "ChatReasonEffortLevel.Low":
+ reasoningEffortObject = ChatReasoningEffortLevel.Low;
+ break;
+ case "ChatReasonEffortLevel.Medium":
+ reasoningEffortObject = ChatReasoningEffortLevel.Medium;
+ break;
+ case "ChatReasonEffortLevel.High":
+ reasoningEffortObject = ChatReasoningEffortLevel.High;
+ break;
+ }
+
+ var modelId = "o1";
+ var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient);
+ OpenAIPromptExecutionSettings executionSettings = new() { ReasoningEffort = reasoningEffortObject };
+
+ this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json"))
+ };
+
+ // Act
+ var result = await sut.GetChatMessageContentAsync(this._chatHistoryForTest, executionSettings);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!);
+ Assert.NotNull(actualRequestContent);
+
+ var optionsJson = JsonSerializer.Deserialize(actualRequestContent);
+
+ if (expectedEffortLevel is null)
+ {
+ Assert.False(optionsJson.TryGetProperty("reasoning_effort", out _));
+ return;
+ }
+
+ var requestedReasoningEffort = optionsJson.GetProperty("reasoning_effort").GetString();
+
+ Assert.Equal(expectedEffortLevel, requestedReasoningEffort);
+ }
+
[Fact(Skip = "Not working running in the console")]
public async Task GetInvalidResponseThrowsExceptionAndIsCapturedByDiagnosticsAsync()
{
diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs
index 90272b94717c..dda1af38a596 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs
@@ -34,6 +34,10 @@ public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults()
Assert.Equal(128, executionSettings.MaxTokens);
Assert.Null(executionSettings.Store);
Assert.Null(executionSettings.Metadata);
+ Assert.Null(executionSettings.Seed);
+ Assert.Null(executionSettings.ReasoningEffort);
+ Assert.Null(executionSettings.ChatSystemPrompt);
+ Assert.Null(executionSettings.ChatDeveloperPrompt);
}
[Fact]
@@ -48,13 +52,15 @@ public void ItUsesExistingOpenAIExecutionSettings()
PresencePenalty = 0.7,
StopSequences = ["foo", "bar"],
ChatSystemPrompt = "chat system prompt",
+ ChatDeveloperPrompt = "chat developer prompt",
MaxTokens = 128,
Logprobs = true,
TopLogprobs = 5,
TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } },
Seed = 123456,
Store = true,
- Metadata = new Dictionary() { { "foo", "bar" } }
+ Metadata = new Dictionary() { { "foo", "bar" } },
+ ReasoningEffort = "high"
};
// Act
@@ -70,6 +76,9 @@ public void ItUsesExistingOpenAIExecutionSettings()
Assert.Equal(actualSettings.Seed, executionSettings.Seed);
Assert.Equal(actualSettings.Store, executionSettings.Store);
Assert.Equal(actualSettings.Metadata, executionSettings.Metadata);
+ Assert.Equal(actualSettings.ReasoningEffort, executionSettings.ReasoningEffort);
+ Assert.Equal(actualSettings.ChatSystemPrompt, executionSettings.ChatSystemPrompt);
+ Assert.Equal(actualSettings.ChatDeveloperPrompt, executionSettings.ChatDeveloperPrompt);
}
[Fact]
@@ -112,6 +121,8 @@ public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase()
{ "results_per_prompt", 2 },
{ "stop_sequences", new [] { "foo", "bar" } },
{ "chat_system_prompt", "chat system prompt" },
+ { "chat_developer_prompt", "chat developer prompt" },
+ { "reasoning_effort", "high" },
{ "max_tokens", 128 },
{ "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } },
{ "seed", 123456 },
@@ -144,6 +155,8 @@ public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings()
{ "results_per_prompt", "2" },
{ "stop_sequences", new [] { "foo", "bar" } },
{ "chat_system_prompt", "chat system prompt" },
+ { "chat_developer_prompt", "chat developer prompt" },
+ { "reasoning_effort", "high" },
{ "max_tokens", "128" },
{ "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } },
{ "seed", 123456 },
@@ -174,6 +187,8 @@ public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase()
"results_per_prompt": 2,
"stop_sequences": [ "foo", "bar" ],
"chat_system_prompt": "chat system prompt",
+ "chat_developer_prompt": "chat developer prompt",
+ "reasoning_effort": "high",
"token_selection_biases": { "1": 2, "3": 4 },
"max_tokens": 128,
"seed": 123456,
@@ -311,6 +326,8 @@ private static void AssertExecutionSettings(OpenAIPromptExecutionSettings execut
Assert.Equal(0.7, executionSettings.PresencePenalty);
Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences);
Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt);
+ Assert.Equal("chat developer prompt", executionSettings.ChatDeveloperPrompt);
+ Assert.Equal("high", executionSettings.ReasoningEffort!.ToString());
Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases);
Assert.Equal(128, executionSettings.MaxTokens);
Assert.Equal(123456, executionSettings.Seed);
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
index 129e7913b788..683e44bfe32b 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs
@@ -469,6 +469,7 @@ protected virtual ChatCompletionOptions CreateChatCompletionOptions(
TopLogProbabilityCount = executionSettings.TopLogprobs,
IncludeLogProbabilities = executionSettings.Logprobs,
StoredOutputEnabled = executionSettings.Store,
+ ReasoningEffortLevel = GetEffortLevel(executionSettings),
};
var responseFormat = GetResponseFormat(executionSettings);
@@ -519,6 +520,33 @@ protected virtual ChatCompletionOptions CreateChatCompletionOptions(
return options;
}
+ protected static ChatReasoningEffortLevel? GetEffortLevel(OpenAIPromptExecutionSettings executionSettings)
+ {
+ var effortLevelObject = executionSettings.ReasoningEffort;
+ if (effortLevelObject is null)
+ {
+ return null;
+ }
+
+ if (effortLevelObject is ChatReasoningEffortLevel effort)
+ {
+ return effort;
+ }
+
+ if (effortLevelObject is string textEffortLevel)
+ {
+ return textEffortLevel.ToUpperInvariant() switch
+ {
+ "LOW" => ChatReasoningEffortLevel.Low,
+ "MEDIUM" => ChatReasoningEffortLevel.Medium,
+ "HIGH" => ChatReasoningEffortLevel.High,
+ _ => throw new NotSupportedException($"The provided reasoning effort '{textEffortLevel}' is not supported.")
+ };
+ }
+
+ throw new NotSupportedException($"The provided reasoning effort '{effortLevelObject.GetType()}' is not supported.");
+ }
+
///
/// Retrieves the response format based on the provided settings.
///
@@ -589,13 +617,14 @@ private static bool IsRequestableTool(IList tools, FunctionCallContent
///
/// Optional chat instructions for the AI service
/// Execution settings
+ /// Indicates what will be the role of the text. Defaults to system role prompt
/// Chat object
- private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null)
+ private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null, AuthorRole? textRole = null)
{
var chat = new ChatHistory();
// If settings is not provided, create a new chat with the text as the system prompt
- AuthorRole textRole = AuthorRole.System;
+ textRole ??= AuthorRole.System;
if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt))
{
@@ -603,9 +632,15 @@ private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecut
textRole = AuthorRole.User;
}
+ if (!string.IsNullOrWhiteSpace(executionSettings?.ChatDeveloperPrompt))
+ {
+ chat.AddDeveloperMessage(executionSettings!.ChatDeveloperPrompt!);
+ textRole = AuthorRole.User;
+ }
+
if (!string.IsNullOrWhiteSpace(text))
{
- chat.AddMessage(textRole, text!);
+ chat.AddMessage(textRole.Value, text!);
}
return chat;
@@ -615,6 +650,11 @@ private static List CreateChatCompletionMessages(OpenAIPromptExecut
{
List messages = [];
+ if (!string.IsNullOrWhiteSpace(executionSettings.ChatDeveloperPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.Developer))
+ {
+ messages.Add(new DeveloperChatMessage(executionSettings.ChatDeveloperPrompt));
+ }
+
if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System))
{
messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt));
@@ -630,6 +670,11 @@ private static List CreateChatCompletionMessages(OpenAIPromptExecut
private static List CreateRequestMessages(ChatMessageContent message)
{
+ if (message.Role == AuthorRole.Developer)
+ {
+ return [new DeveloperChatMessage(message.Content) { ParticipantName = message.AuthorName }];
+ }
+
if (message.Role == AuthorRole.System)
{
return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }];
diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs
index add62d564046..bd3187b936d6 100644
--- a/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs
+++ b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs
@@ -18,6 +18,29 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI;
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public class OpenAIPromptExecutionSettings : PromptExecutionSettings
{
+ ///
+ /// Gets or sets an object specifying the effort level for the model to use when generating the completion.
+ ///
+ ///
+ /// Constrains effort on reasoning for reasoning models.
+ /// Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.
+ /// Possible values are:
+ /// - values: "low", "medium", "high";
+ /// - object;
+ ///
+ [Experimental("SKEXP0010")]
+ [JsonPropertyName("reasoning_effort")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public object? ReasoningEffort
+ {
+ get => this._reasoningEffort;
+ set
+ {
+ this.ThrowIfFrozen();
+ this._reasoningEffort = value;
+ }
+ }
+
///
/// Temperature controls the randomness of the completion.
/// The higher the temperature, the more random the completion.
@@ -183,6 +206,24 @@ public string? ChatSystemPrompt
}
}
+ ///
+ /// The system prompt to use when generating text using a chat model.
+ /// Defaults to "Assistant is a large language model."
+ ///
+ [Experimental("SKEXP0010")]
+ [JsonPropertyName("chat_developer_prompt")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ChatDeveloperPrompt
+ {
+ get => this._chatDeveloperPrompt;
+
+ set
+ {
+ this.ThrowIfFrozen();
+ this._chatDeveloperPrompt = value;
+ }
+ }
+
///
/// Modify the likelihood of specified tokens appearing in the completion.
///
@@ -410,15 +451,18 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio
FunctionChoiceBehavior = this.FunctionChoiceBehavior,
User = this.User,
ChatSystemPrompt = this.ChatSystemPrompt,
+ ChatDeveloperPrompt = this.ChatDeveloperPrompt,
Logprobs = this.Logprobs,
TopLogprobs = this.TopLogprobs,
Store = this.Store,
Metadata = this.Metadata is not null ? new Dictionary(this.Metadata) : null,
+ ReasoningEffort = this.ReasoningEffort
};
}
#region private ================================================================================
+ private object? _reasoningEffort;
private double? _temperature;
private double? _topP;
private double? _presencePenalty;
@@ -431,6 +475,7 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio
private ToolCallBehavior? _toolCallBehavior;
private string? _user;
private string? _chatSystemPrompt;
+ private string? _chatDeveloperPrompt;
private bool? _logprobs;
private int? _topLogprobs;
private bool? _store;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AuthorRole.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AuthorRole.cs
index 05f473b1b792..d4dd082dd98b 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AuthorRole.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/AuthorRole.cs
@@ -11,6 +11,11 @@ namespace Microsoft.SemanticKernel.ChatCompletion;
///
public readonly struct AuthorRole : IEquatable
{
+ ///
+ /// The role that instructs or sets the behavior of the assistant.
+ ///
+ public static AuthorRole Developer { get; } = new("developer");
+
///
/// The role that instructs or sets the behavior of the assistant.
///
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs
index fda7be0d0c8c..22968c47ea38 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs
@@ -28,15 +28,26 @@ public ChatHistory()
}
///
- /// Creates a new instance of the class with a system message
+ /// Creates a new instance of the with a first message in the provided .
+ /// If not role is provided then the first message will default to role.
///
- /// The system message to add to the history.
- public ChatHistory(string systemMessage)
+ /// The text message to add to the first message in chat history.
+ /// The role to add as the first message.
+ public ChatHistory(string message, AuthorRole role)
{
- Verify.NotNullOrWhiteSpace(systemMessage);
+ Verify.NotNullOrWhiteSpace(message);
this._messages = [];
- this.AddSystemMessage(systemMessage);
+ this.Add(new ChatMessageContent(role, message));
+ }
+
+ ///
+ /// Creates a new instance of the class with a system message.
+ ///
+ /// The system message to add to the history.
+ public ChatHistory(string systemMessage)
+ : this(systemMessage, AuthorRole.System)
+ {
}
/// Initializes the history will all of the specified messages.
@@ -97,6 +108,13 @@ public void AddAssistantMessage(string content) =>
public void AddSystemMessage(string content) =>
this.AddMessage(AuthorRole.System, content);
+ ///
+ /// Add a developer message to the chat history
+ ///
+ /// Message content
+ public void AddDeveloperMessage(string content) =>
+ this.AddMessage(AuthorRole.Developer, content);
+
/// Adds a message to the history.
/// The message to add.
/// is null.
diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs
index 723349450e99..20cc5b1269fd 100644
--- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs
+++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs
@@ -43,4 +43,35 @@ public void ItCanBeSerializedAndDeserialized()
chatHistoryDeserialized[i].Items.OfType().Single().Text);
}
}
+
+ [Theory]
+ [InlineData("system")]
+ [InlineData("developer")]
+ public void CtorWorksForSystemAndDeveloper(string providedRole)
+ {
+ // Arrange
+ var targetRole = providedRole == "system" ? AuthorRole.System : AuthorRole.Developer;
+ var options = new JsonSerializerOptions();
+ var chatHistory = new ChatHistory("First message", targetRole);
+
+ var chatHistoryJson = JsonSerializer.Serialize(chatHistory, options);
+
+ // Act
+ var chatHistoryDeserialized = JsonSerializer.Deserialize(chatHistoryJson, options);
+
+ // Assert
+ Assert.NotNull(chatHistoryDeserialized);
+ Assert.Equal(chatHistory.Count, chatHistoryDeserialized.Count);
+ Assert.Equal(providedRole, chatHistoryDeserialized[0].Role.Label);
+ for (var i = 0; i < chatHistory.Count; i++)
+ {
+ Assert.Equal(chatHistory[i].Role.Label, chatHistoryDeserialized[i].Role.Label);
+ Assert.Equal(chatHistory[i].Content, chatHistoryDeserialized[i].Content);
+ Assert.Equal(chatHistory[i].AuthorName, chatHistoryDeserialized[i].AuthorName);
+ Assert.Equal(chatHistory[i].Items.Count, chatHistoryDeserialized[i].Items.Count);
+ Assert.Equal(
+ chatHistory[i].Items.OfType().Single().Text,
+ chatHistoryDeserialized[i].Items.OfType().Single().Text);
+ }
+ }
}