Skip to content

Commit

Permalink
OpenAI-DotNet 8.2.0 (#347)
Browse files Browse the repository at this point in the history
- Added structured output support
- Added support for Azure OpenAI assistants
- Fixed Azure OpenAI Id parsing for events
- Fixed Assistant.CreateThreadAndRunAsync to properly copy assistant
parameters
- Removed stream from CreateThreadAndRunRequest and CreateRunRequest
  - They were overridden by the presence of IStreamEventHandler anyway

## OpenAI-DotNet-Proxy 8.2.0
 
 - Deprecated ValidateAuthentication for ValidateAuthenticationAsync
  • Loading branch information
StephenHodgson authored Aug 18, 2024
1 parent 8fcd8d8 commit 081a3d6
Show file tree
Hide file tree
Showing 36 changed files with 812 additions and 285 deletions.
4 changes: 3 additions & 1 deletion OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
<IncludeSymbols>true</IncludeSymbols>
<SignAssembly>false</SignAssembly>
<ImplicitUsings>false</ImplicitUsings>
<Version>8.1.1</Version>
<Version>8.2.0</Version>
<PackageReleaseNotes>
Version 8.2.0
- Deprecated ValidateAuthentication for ValidateAuthenticationAsync
Version 8.1.1
- Renamed OpenAIProxyStartup to OpenAIProxy
Version 7.7.10
Expand Down
5 changes: 3 additions & 2 deletions OpenAI-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;

namespace OpenAI.Proxy
{
/// <inheritdoc />
public abstract class AbstractAuthenticationFilter : IAuthenticationFilter
{
/// <inheritdoc />
public abstract void ValidateAuthentication(IHeaderDictionary request);
[Obsolete("Use ValidateAuthenticationAsync")]
public virtual void ValidateAuthentication(IHeaderDictionary request) { }

/// <inheritdoc />
public abstract Task ValidateAuthenticationAsync(IHeaderDictionary request);
Expand Down
15 changes: 4 additions & 11 deletions OpenAI-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)
{
try
{
#pragma warning disable CS0618 // Type or member is obsolete
// ReSharper disable once MethodHasAsyncOverload
// just in case either method is implemented we call it twice.
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
#pragma warning restore CS0618 // Type or member is obsolete
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);

var method = new HttpMethod(httpContext.Request.Method);
Expand All @@ -80,21 +81,13 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)

foreach (var (key, value) in proxyResponse.Headers)
{
if (excludedHeaders.Contains(key))
{
continue;
}

if (excludedHeaders.Contains(key)) { continue; }
httpContext.Response.Headers[key] = value.ToArray();
}

foreach (var (key, value) in proxyResponse.Content.Headers)
{
if (excludedHeaders.Contains(key))
{
continue;
}

if (excludedHeaders.Contains(key)) { continue; }
httpContext.Response.Headers[key] = value.ToArray();
}

Expand Down
8 changes: 2 additions & 6 deletions OpenAI-DotNet-Proxy/Proxy/IAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Http;
using System;
using System.Security.Authentication;
using System.Threading.Tasks;

Expand All @@ -11,12 +12,7 @@ namespace OpenAI.Proxy
/// </summary>
public interface IAuthenticationFilter
{
/// <summary>
/// Checks the headers for your user issued token.
/// If it's not valid, then throw <see cref="AuthenticationException"/>.
/// </summary>
/// <param name="request"></param>
/// <exception cref="AuthenticationException"></exception>
[Obsolete("Use ValidateAuthenticationAsync")]
void ValidateAuthentication(IHeaderDictionary request);

/// <summary>
Expand Down
13 changes: 2 additions & 11 deletions OpenAI-DotNet-Proxy/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ In this example, we demonstrate how to set up and use `OpenAIProxy` in a new ASP
1. Create a new [ASP.NET Core minimal web API](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?view=aspnetcore-6.0) project.
2. Add the OpenAI-DotNet nuget package to your project.
- Powershell install: `Install-Package OpenAI-DotNet-Proxy`
- Dotnet install: `dotnet add package OpenAI-DotNet-Proxy`
- Manually editing .csproj: `<PackageReference Include="OpenAI-DotNet-Proxy" />`
3. Create a new class that inherits from `AbstractAuthenticationFilter` and override the `ValidateAuthentication` method. This will implement the `IAuthenticationFilter` that you will use to check user session token against your internal server.
4. In `Program.cs`, create a new proxy web application by calling `OpenAIProxy.CreateWebApplication` method, passing your custom `AuthenticationFilter` as a type argument.
Expand All @@ -59,19 +60,9 @@ public partial class Program
{
private class AuthenticationFilter : AbstractAuthenticationFilter
{
public override void ValidateAuthentication(IHeaderDictionary request)
{
// You will need to implement your own class to properly test
// custom issued tokens you've setup for your end users.
if (!request.Authorization.ToString().Contains(TestUserToken))
{
throw new AuthenticationException("User is not authorized");
}
}

public override async Task ValidateAuthenticationAsync(IHeaderDictionary request)
{
await Task.CompletedTask; // remote resource call
await Task.CompletedTask; // remote resource call to verify token
// You will need to implement your own class to properly test
// custom issued tokens you've setup for your end users.
Expand Down
13 changes: 2 additions & 11 deletions OpenAI-DotNet-Tests-Proxy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,10 @@ public partial class Program

private class AuthenticationFilter : AbstractAuthenticationFilter
{
public override void ValidateAuthentication(IHeaderDictionary request)
{
// You will need to implement your own class to properly test
// custom issued tokens you've setup for your end users.
if (!request.Authorization.ToString().Contains(TestUserToken))
{
throw new AuthenticationException("User is not authorized");
}
}

/// <inheritdoc />
public override async Task ValidateAuthenticationAsync(IHeaderDictionary request)
{
await Task.CompletedTask; // remote resource call
await Task.CompletedTask; // remote resource call to verify token

// You will need to implement your own class to properly test
// custom issued tokens you've setup for your end users.
Expand Down
1 change: 1 addition & 0 deletions OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public void Test_11_AzureConfigurationSettings()
var auth = new OpenAIAuthentication("testKeyAaBbCcDd");
var settings = new OpenAIClientSettings(resourceName: "test-resource", deploymentId: "deployment-id-test");
var api = new OpenAIClient(auth, settings);
Console.WriteLine(api.OpenAIClientSettings.DeploymentId);
Console.WriteLine(api.OpenAIClientSettings.BaseRequest);
Console.WriteLine(api.OpenAIClientSettings.BaseRequestUrlFormat);
}
Expand Down
80 changes: 80 additions & 0 deletions OpenAI-DotNet-Tests/TestFixture_03_Threads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -667,5 +667,85 @@ public async Task Test_04_04_CreateThreadAndRun_SubmitToolOutput()
}
}
}

[Test]
public async Task Test_05_01_CreateThreadAndRun_StructuredOutputs_Streaming()
{
Assert.NotNull(OpenAIClient.ThreadsEndpoint);
var mathSchema = new JsonSchema("math_response", @"
{
""type"": ""object"",
""properties"": {
""steps"": {
""type"": ""array"",
""items"": {
""type"": ""object"",
""properties"": {
""explanation"": {
""type"": ""string""
},
""output"": {
""type"": ""string""
}
},
""required"": [
""explanation"",
""output""
],
""additionalProperties"": false
}
},
""final_answer"": {
""type"": ""string""
}
},
""required"": [
""steps"",
""final_answer""
],
""additionalProperties"": false
}");
var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(
new CreateAssistantRequest(
name: "Math Tutor",
instructions: "You are a helpful math tutor. Guide the user through the solution step by step.",
model: "gpt-4o-2024-08-06",
jsonSchema: mathSchema));
Assert.NotNull(assistant);
ThreadResponse thread = null;

try
{
var run = await assistant.CreateThreadAndRunAsync("how can I solve 8x + 7 = -23",
async @event =>
{
Console.WriteLine(@event.ToJsonString());
await Task.CompletedTask;
});
Assert.IsNotNull(run);
thread = await run.GetThreadAsync();
run = await run.WaitForStatusChangeAsync();
Assert.IsNotNull(run);
Assert.IsTrue(run.Status == RunStatus.Completed);
Console.WriteLine($"Created thread and run: {run.ThreadId} -> {run.Id} -> {run.CreatedAt}");
Assert.NotNull(thread);
var messages = await thread.ListMessagesAsync();

foreach (var response in messages.Items)
{
Console.WriteLine($"{response.Role}: {response.PrintContent()}");
}
}
finally
{
await assistant.DeleteAsync(deleteToolResources: thread == null);

if (thread != null)
{
var isDeleted = await thread.DeleteAsync(deleteToolResources: true);
Assert.IsTrue(isDeleted);
}
}
}
}
}
131 changes: 130 additions & 1 deletion OpenAI-DotNet-Tests/TestFixture_04_Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
};

var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
var chatRequest = new ChatRequest(messages, model: Model.GPT4o, tools: tools, toolChoice: "auto");
var chatRequest = new ChatRequest(messages, model: Model.GPT4o, tools: tools, toolChoice: "auto", parallelToolCalls: true);
var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
{
Assert.IsNotNull(partialResponse);
Expand Down Expand Up @@ -526,5 +526,134 @@ public async Task Test_05_02_GetChat_Enumerable_TestToolCalls_Streaming()
}
}
}

[Test]
public async Task Test_06_01_GetChat_JsonSchema()
{
Assert.IsNotNull(OpenAIClient.ChatEndpoint);

var messages = new List<Message>
{
new(Role.System, "You are a helpful math tutor. Guide the user through the solution step by step."),
new(Role.User, "how can I solve 8x + 7 = -23")
};

var mathSchema = new JsonSchema("math_response", @"
{
""type"": ""object"",
""properties"": {
""steps"": {
""type"": ""array"",
""items"": {
""type"": ""object"",
""properties"": {
""explanation"": {
""type"": ""string""
},
""output"": {
""type"": ""string""
}
},
""required"": [
""explanation"",
""output""
],
""additionalProperties"": false
}
},
""final_answer"": {
""type"": ""string""
}
},
""required"": [
""steps"",
""final_answer""
],
""additionalProperties"": false
}");
var chatRequest = new ChatRequest(messages, model: new("gpt-4o-2024-08-06"), jsonSchema: mathSchema);
var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
Assert.IsNotNull(response);
Assert.IsNotNull(response.Choices);
Assert.IsNotEmpty(response.Choices);

foreach (var choice in response.Choices)
{
Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}");
}

response.GetUsage();
}

[Test]
public async Task Test_06_01_GetChat_JsonSchema_Streaming()
{
Assert.IsNotNull(OpenAIClient.ChatEndpoint);

var messages = new List<Message>
{
new(Role.System, "You are a helpful math tutor. Guide the user through the solution step by step."),
new(Role.User, "how can I solve 8x + 7 = -23")
};

var mathSchema = new JsonSchema("math_response", @"
{
""type"": ""object"",
""properties"": {
""steps"": {
""type"": ""array"",
""items"": {
""type"": ""object"",
""properties"": {
""explanation"": {
""type"": ""string""
},
""output"": {
""type"": ""string""
}
},
""required"": [
""explanation"",
""output""
],
""additionalProperties"": false
}
},
""final_answer"": {
""type"": ""string""
}
},
""required"": [
""steps"",
""final_answer""
],
""additionalProperties"": false
}");
var chatRequest = new ChatRequest(messages, model: "gpt-4o-2024-08-06", jsonSchema: mathSchema);
var cumulativeDelta = string.Empty;
var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
{
Assert.IsNotNull(partialResponse);
if (partialResponse.Usage != null) { return; }
Assert.NotNull(partialResponse.Choices);
Assert.NotZero(partialResponse.Choices.Count);

foreach (var choice in partialResponse.Choices.Where(choice => choice.Delta?.Content != null))
{
cumulativeDelta += choice.Delta.Content;
}
}, true);
Assert.IsNotNull(response);
Assert.IsNotNull(response.Choices);
var choice = response.FirstChoice;
Assert.IsNotNull(choice);
Assert.IsNotNull(choice.Message);
Assert.IsFalse(string.IsNullOrEmpty(choice.ToString()));
Console.WriteLine($"[{choice.Index}] {choice.Message.Role}: {choice} | Finish Reason: {choice.FinishReason}");
Assert.IsTrue(choice.Message.Role == Role.Assistant);
Assert.IsTrue(choice.Message.Content!.Equals(cumulativeDelta));
Console.WriteLine(response.ToString());
response.GetUsage();
}
}
}
Loading

0 comments on commit 081a3d6

Please sign in to comment.