Skip to content

Commit

Permalink
Add initial Azure support; Update to .NET 8
Browse files Browse the repository at this point in the history
  • Loading branch information
rodion-m committed Nov 17, 2023
1 parent cc6bc62 commit 0ae319d
Show file tree
Hide file tree
Showing 25 changed files with 199 additions and 89 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_ENDPOINT_URL: ${{ secrets.AZURE_OPENAI_ENDPOINT_URL }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }}
run: dotnet test --no-build --verbosity normal
4 changes: 2 additions & 2 deletions .github/workflows/publish_to_nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Publish NuGet Package

on:
release:
types: [ ]
types: [ created ]
push:
branches: [ release ]

Expand All @@ -17,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
7 changes: 7 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}
4 changes: 2 additions & 2 deletions samples/ChatGpt.BlazorExample/ChatGpt.BlazorExample.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>e883db38-8c66-433b-a1cf-301b5b3b2171</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
<PackageReference Include="OpenAI.ChatGPT.EntityFrameworkCore" Version="2.8.0" />
<PackageReference Include="OpenAI.ChatGPT.EntityFrameworkCore" Version="3.1.1" />
</ItemGroup>

</Project>
3 changes: 1 addition & 2 deletions samples/ChatGpt.BlazorExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
// make sure that you have added the OpenAICredentials:ApiKey to your secrets.json
// or environment variables
builder.Services.AddChatGptEntityFrameworkIntegration(
options => options.UseSqlite("Data Source=chats.db"))
.AddServerSideBlazor();
options => options.UseSqlite("Data Source=chats.db"));

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
<PackageReference Include="OpenAI.ChatGPT.EntityFrameworkCore" Version="2.8.0" />
<PackageReference Include="OpenAI.ChatGPT.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Telegram.Bot" Version="19.0.0" />
</ItemGroup>

Expand Down
3 changes: 2 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<PropertyGroup>
<Version>3.1.1</Version>
<Version>3.2.0</Version>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
Expand Down
12 changes: 5 additions & 7 deletions src/OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<Authors>Rodion Mostovoi</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>OpenAI.ChatGPT.AspNetCore</PackageId>
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI ChatGPT integration for .NET with DI</Product>
<Description>OpenAI Chat Completions API (ChatGPT) integration with easy DI supporting (Microsoft.Extensions.DependencyInjection). It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<PackageTags>chatgpt, openai, sdk, api, chatcompletions, gpt3, gpt4, aspnetcore</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Title>ChatGPT easy DI for ASP.NET Core</Title>
Expand All @@ -36,11 +34,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0"/>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<Authors>Rodion Mostovoi</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>OpenAI.ChatGPT.EntityFrameworkCore</PackageId>
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI ChatGPT integration for .NET with EF Core storage</Product>
<Description>OpenAI Chat Completions API (ChatGPT) integration with DI and EF Core supporting. It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<PackageTags>chatgpt, openai, sdk, api, chatcompletions, gpt3, gpt4, di, entityframework, ef</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Title>ChatGPT easy DI with EF Core storage</Title>
Expand All @@ -31,8 +29,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
57 changes: 57 additions & 0 deletions src/OpenAI.ChatGpt/AzureOpenAiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace OpenAI.ChatGpt;

/// <summary>
/// Azure OpenAI services client
/// </summary>
/// <remarks>
/// Docs: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
/// Models availability by zones: https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models
/// </remarks>
public class AzureOpenAiClient : OpenAiClient
{
private readonly string _apiVersion;
private const string DefaultApiVersion = "2023-12-01-preview";

//https://github.com/Azure/azure-rest-api-specs/tree/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference

/// <summary>
/// Creates Azure OpenAI services client
/// </summary>
/// <param name="endpointUrl"> Endpoint URL like https://{your-resource-name}.openai.azure.com/</param>
/// <param name="deploymentName"> Deployment name from the page https://oai.azure.com/deployment</param>
/// <param name="azureKey">Azure OpenAI API Key</param>
/// <param name="apiVersion">Azure OpenAI API version</param>
/// <remarks>
/// See currently available API versions: https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#completions
/// </remarks>
public AzureOpenAiClient(
string endpointUrl,
string deploymentName,
string azureKey,
string apiVersion = DefaultApiVersion)
{
if (string.IsNullOrWhiteSpace(azureKey))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(azureKey));

_apiVersion = apiVersion ?? throw new ArgumentNullException(nameof(apiVersion));
HttpClient = new HttpClient()
{
BaseAddress = new Uri($"{endpointUrl}/openai/deployments/{deploymentName}/"),
DefaultRequestHeaders =
{
{ "api-key", azureKey }
}
};
IsHttpClientInjected = false;
}

public AzureOpenAiClient(HttpClient httpClient, string apiVersion) : base(httpClient)
{
_apiVersion = apiVersion ?? throw new ArgumentNullException(nameof(apiVersion));
}

protected override string GetChatCompletionsEndpoint()
{
return $"chat/completions?api-version={_apiVersion}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ public int MaxTokens
/// <summary>
/// An object specifying the format that the model must output.
/// </summary>
[JsonPropertyName("response_format")]
public ChatCompletionResponseFormat ResponseFormat { get; set; } = new(false);
[JsonPropertyName("response_format"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public ChatCompletionResponseFormat? ResponseFormat { get; set; }

/// <summary>
/// This feature is in Beta.
Expand Down
2 changes: 0 additions & 2 deletions src/OpenAI.ChatGpt/OpenAI.ChatGpt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<Authors>Rodion Mostovoi</Authors>
<Title>OpenAI ChatGPT integration for .NET</Title>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand All @@ -13,7 +12,6 @@
<Description>.NET integration for ChatGPT with streaming responses supporting (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<RootNamespace>OpenAI.ChatGpt</RootNamespace>
<Copyright>Rodion Mostovoi</Copyright>
<PackageTags>chatgpt, openai, sdk, api, chatcompletions, gpt3, gpt4</PackageTags>
Expand Down
46 changes: 29 additions & 17 deletions src/OpenAI.ChatGpt/OpenAiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@ public class OpenAiClient : IOpenAiClient, IDisposable

private static readonly Uri DefaultHostUri = new(DefaultHost);

private readonly HttpClient _httpClient;
private readonly bool _isHttpClientInjected;
protected HttpClient HttpClient;
protected bool IsHttpClientInjected;
private bool _disposed;

private readonly JsonSerializerOptions _nullIgnoreSerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected OpenAiClient()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
}

/// <summary>
/// Creates a new OpenAI client with given <paramref name="apiKey"/>.
/// </summary>
Expand All @@ -39,38 +45,38 @@ public OpenAiClient(string apiKey, string? host = DefaultHost)
throw new ArgumentException("API key cannot be null or whitespace.", nameof(apiKey));
var uri = ValidateHost(host);

_httpClient = new HttpClient()
HttpClient = new HttpClient()
{
BaseAddress = uri
};
var header = new AuthenticationHeaderValue("Bearer", apiKey);
_httpClient.DefaultRequestHeaders.Authorization = header;
HttpClient.DefaultRequestHeaders.Authorization = header;
}

/// <summary>
/// Creates a new OpenAI client from DI with given <paramref name="httpClient"/>.
/// </summary>
/// <param name="httpClient">
/// <see cref="HttpClient"/> from DI. It should have an Authorization header set with OpenAI API key.
/// <see cref="System.Net.Http.HttpClient"/> from DI. It should have an Authorization header set with OpenAI API key.
/// </param>
/// <exception cref="ArgumentException">
/// Indicates that OpenAI API key is not set in
/// <paramref name="httpClient"/>.<see cref="HttpClient.DefaultRequestHeaders"/>.<see cref="HttpRequestHeaders.Authorization"/> header.
/// </exception>
public OpenAiClient(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
ValidateHttpClient(httpClient);
_isHttpClientInjected = true;
IsHttpClientInjected = true;
}

public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (!_isHttpClientInjected)
if (!IsHttpClientInjected)
{
_httpClient.Dispose();
HttpClient.Dispose();
}
GC.SuppressFinalize(this);
}
Expand All @@ -89,8 +95,9 @@ private static Uri ValidateHost(string? host)
if (host is null) return DefaultHostUri;
if (!Uri.TryCreate(host, UriKind.Absolute, out var uri))
{
throw new ArgumentException("Host must be a valid absolute URI and end with a slash." +
$"For example: {DefaultHost}", nameof(host));
throw new ArgumentException(
$"Host must be a valid absolute URI and end with a slash. Provided: {host}" +
$"\nCorrect example: {DefaultHost}", nameof(host));
}
if(!host.EndsWith("/")) uri = new Uri(host + "/");

Expand Down Expand Up @@ -230,8 +237,8 @@ internal async Task<ChatCompletionResponse> GetChatCompletionsRaw(
{
ArgumentNullException.ThrowIfNull(request);
ThrowIfDisposed();
var response = await _httpClient.PostAsJsonAsync(
ChatCompletionsEndpoint,
var response = await HttpClient.PostAsJsonAsync(
GetChatCompletionsEndpoint(),
request,
cancellationToken: cancellationToken,
options: _nullIgnoreSerializerOptions
Expand All @@ -247,6 +254,11 @@ internal async Task<ChatCompletionResponse> GetChatCompletionsRaw(
return jsonResponse;
}

protected virtual string GetChatCompletionsEndpoint()
{
return ChatCompletionsEndpoint;
}

/// <inheritdoc />
public IAsyncEnumerable<string> StreamChatCompletions(
IEnumerable<ChatCompletionMessage> messages,
Expand Down Expand Up @@ -296,7 +308,7 @@ private static ChatCompletionRequest CreateChatCompletionRequest(
Stream = stream,
User = user,
Temperature = temperature,
ResponseFormat = new ChatCompletionRequest.ChatCompletionResponseFormat(jsonMode),
ResponseFormat = jsonMode ? new ChatCompletionRequest.ChatCompletionResponseFormat(jsonMode) : null,
Seed = seed,
};
requestModifier?.Invoke(request);
Expand Down Expand Up @@ -339,7 +351,7 @@ public async IAsyncEnumerable<string> StreamChatCompletions(
{
ArgumentNullException.ThrowIfNull(request);
if (request == null) throw new ArgumentNullException(nameof(request));
EnsureJsonModeIsSupported(request.Model, request.ResponseFormat.Type == ChatCompletionRequest.ResponseTypes.JsonObject);
EnsureJsonModeIsSupported(request.Model, request.ResponseFormat?.Type == ChatCompletionRequest.ResponseTypes.JsonObject);
ThrowIfDisposed();
request.Stream = true;
await foreach (var response in StreamChatCompletionsRaw(request, cancellationToken))
Expand All @@ -355,10 +367,10 @@ public IAsyncEnumerable<ChatCompletionResponse> StreamChatCompletionsRaw(
ChatCompletionRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
EnsureJsonModeIsSupported(request.Model, request.ResponseFormat.Type == ChatCompletionRequest.ResponseTypes.JsonObject);
EnsureJsonModeIsSupported(request.Model, request.ResponseFormat?.Type == ChatCompletionRequest.ResponseTypes.JsonObject);
ThrowIfDisposed();
request.Stream = true;
return _httpClient.StreamUsingServerSentEvents<ChatCompletionRequest, ChatCompletionResponse>
return HttpClient.StreamUsingServerSentEvents<ChatCompletionRequest, ChatCompletionResponse>
(
ChatCompletionsEndpoint,
request,
Expand Down
Loading

0 comments on commit 0ae319d

Please sign in to comment.