diff --git a/.editorconfig b/.editorconfig index c2c949e1..6f823942 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,7 +22,7 @@ csharp_new_line_before_finally = true csharp_new_line_before_open_brace = all # Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_require_accessibility_modifiers = error # Code-block preferences csharp_prefer_braces = true:error diff --git a/.github/workflows/Publish-Nuget.yml b/.github/workflows/Publish-Nuget.yml index ceadaa9d..9b9bf693 100644 --- a/.github/workflows/Publish-Nuget.yml +++ b/.github/workflows/Publish-Nuget.yml @@ -41,24 +41,49 @@ env: jobs: build: if: ${{ !github.event_name == 'pull_request' || !github.event.pull_request.draft }} - env: - PACKAGE_VERSION: '' - COVERAGE_FILE_PATH: '' runs-on: ubuntu-latest - + defaults: + run: + shell: pwsh steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - run: dotnet restore - - run: dotnet build --configuration Release --no-restore + - name: Get Version + id: version + run: | + $projectPath = "${{ github.workspace }}\OpenAI-DotNet\OpenAI-DotNet.csproj" + + if (-Not (Test-Path $projectPath)) { + Write-Host "Project file not found at $projectPath" + exit 1 + } + + [xml]$csproj = Get-Content $projectPath + + if ($csproj -eq $null) { + Write-Host "Failed to load csproj file." + exit 1 + } + + $version = $csproj.Project.PropertyGroup.Version + + if ([string]::IsNullOrEmpty($version)) { + Write-Host "Version not found in csproj." + exit 1 + } + + Write-Host "Project Version: $version" + echo "PACKAGE_VERSION=$version" >> $GITHUB_OUTPUT + - name: Test Packages if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' }} run: dotnet test --configuration Release --collect:"XPlat Code Coverage" --logger:trx --no-build --no-restore --results-directory ./test-results @@ -68,83 +93,82 @@ jobs: - name: Publish Test Results if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} - uses: EnricoMi/publish-unit-test-result-action@v2 + uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.11.0 with: files: test-results/**/*.trx comment_mode: off report_individual_runs: true compare_to_earlier_commit: false + large_files: true - name: Determine Coverage File Path if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} + id: coverage-path shell: bash run: | COVERAGE_FILE_PATH=$(find ./test-results -name 'coverage.cobertura.xml' | head -n 1) - echo "COVERAGE_FILE_PATH=$COVERAGE_FILE_PATH" >> $GITHUB_ENV + echo "COVERAGE_FILE_PATH=$COVERAGE_FILE_PATH" >> $GITHUB_OUTPUT - name: Code Coverage Summary Report if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} - uses: irongut/CodeCoverageSummary@v1.3.0 + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 with: - filename: ${{ env.COVERAGE_FILE_PATH }} + filename: ${{ steps.coverage-path.outputs.COVERAGE_FILE_PATH }} badge: true - format: 'markdown' - output: 'both' + format: markdown + output: both - name: Write Coverage Job Summary if: ${{ github.ref != 'refs/heads/main' && github.event_name != 'push' && always() }} + shell: bash run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY - name: Pack and Publish NuGet Package run: | $projectPath = "${{ github.workspace }}\OpenAI-DotNet" $proxyProjectPath = "${{ github.workspace }}\OpenAI-DotNet-Proxy" - + # pack OpenAI-DotNet dotnet pack $projectPath --configuration Release --include-symbols $out = "$projectPath\bin\Release" $packagePath = Get-ChildItem -Path $out -File -Include '*.nupkg' -Exclude '*symbols*' -Recurse -ErrorAction SilentlyContinue - + if ($packagePath) { Write-Host Package path: $packagePath } else { Write-Host Failed to find package at $out exit 1 } - + # pack OpenAI-DotNet-Proxy dotnet pack $proxyProjectPath --configuration Release --include-symbols $proxyOut = "$proxyProjectPath\bin\Release" $proxyPackagePath = Get-ChildItem -Path $proxyOut -File -Include '*.nupkg' -Exclude '*symbols*' -Recurse -ErrorAction SilentlyContinue - + if ($proxyPackagePath) { Write-Host Package path: $proxyPackagePath } else { Write-Host Failed to find package at $proxyOut exit 1 } - + $isRelease = "${{ github.ref == 'refs/heads/main' }}" - + if ($isRelease -eq 'true') { dotnet nuget push $packagePath.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push $proxyPackagePath.FullName --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate } - $version = $packagePath.Name -replace "^OpenAI-DotNet.(.*).nupkg$",'$1' - echo "PACKAGE_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - shell: pwsh - - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 if: always() with: - name: OpenAI-DotNet.${{ env.PACKAGE_VERSION }} + name: OpenAI-DotNet.${{ steps.version.outputs.PACKAGE_VERSION }}-artifacts path: | ${{ github.workspace }}/test-results - ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet.${{ env.PACKAGE_VERSION }}.nupkg - ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet.${{ env.PACKAGE_VERSION }}.symbols.nupkg - ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet-Proxy.${{ env.PACKAGE_VERSION }}.nupkg - ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet-Proxy.${{ env.PACKAGE_VERSION }}.symbols.nupkg + ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet.${{ steps.version.outputs.PACKAGE_VERSION }}.nupkg + ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet.${{ steps.version.outputs.PACKAGE_VERSION }}.symbols.nupkg + ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet-Proxy.${{ steps.version.outputs.PACKAGE_VERSION }}.nupkg + ${{ github.workspace }}/OpenAI-DotNet/bin/Release/OpenAI-DotNet-Proxy.${{ steps.version.outputs.PACKAGE_VERSION }}.symbols.nupkg if-no-files-found: ignore docs: @@ -154,25 +178,23 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: build docfx run: | dotnet tool update -g docfx docfx .docs/docfx.json - - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: path: '_site' - - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4.0.3 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/OpenAI-DotNet/Extensions/BaseEndpointExtensions.cs b/OpenAI-DotNet/Extensions/BaseEndpointExtensions.cs index a27ff909..d14ed31a 100644 --- a/OpenAI-DotNet/Extensions/BaseEndpointExtensions.cs +++ b/OpenAI-DotNet/Extensions/BaseEndpointExtensions.cs @@ -34,6 +34,12 @@ public static async Task StreamEventsAsync( request.Content = payload; var response = await baseEndpoint.ServerSentEventStreamAsync(request, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(false, payload, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (baseEndpoint.EnableDebug) + { + await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); + } + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var events = new Stack(); using var reader = new StreamReader(stream); diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs index b69d3356..ba1d5f86 100644 --- a/OpenAI-DotNet/Models/Model.cs +++ b/OpenAI-DotNet/Models/Model.cs @@ -75,6 +75,18 @@ public Model(string id, string ownedBy = null) #region Reasoning Models + /// + /// GPT-5.2 pro is available in the Responses API only to enable support for multi-turn model interactions before responding to API requests, + /// and other advanced API features in the future. Since GPT-5.2 pro is designed to tackle tough problems, + /// some requests may take several minutes to finish. To avoid timeouts, try using background mode. + /// GPT-5.2 pro supports reasoning.effort: medium, high, xhigh. + /// + /// + /// - Context Window: 400,000 context window
+ /// - Max Output Tokens: 128,000 max output tokens + ///
+ public static Model GPT5_2_Pro { get; } = new("gpt-5.2-pro", "openai"); + /// /// The o1 series of models are trained with reinforcement learning to perform complex reasoning. /// o1 models think before they answer, producing a long internal chain of thought before responding to the user. @@ -189,6 +201,15 @@ public Model(string id, string ownedBy = null) #region Chat Models + /// + /// GPT-5.2 is our flagship model for coding and agentic tasks across industries. + /// + /// + /// - Context Window: 400,000 context window
+ /// - Max Output Tokens: 128,000 max output tokens + ///
+ public static Model GPT5_2 { get; } = new("gpt-5.2", "openai"); + /// /// GPT-5 is our flagship model for coding, reasoning, and agentic tasks across domains. /// @@ -498,6 +519,35 @@ public Model(string id, string ownedBy = null) #region Specialized Models + /// + /// GPT-5.1-Codex-Max is purpose-built for agentic coding. + /// It's only available in the Responses API. + /// + /// + /// - Context Window: 400,000 tokens
+ /// - Max Output Tokens: 128,000 tokens + ///
+ public static Model GPT5_1_CodexMax { get; } = new("gpt-5.1-codex-max", "openai"); + + /// + /// GPT-5.1-Codex is a version of GPT-5 optimized for agentic coding tasks in Codex or similar environments. + /// It's available in the Responses API + /// + /// + /// - Context Window: 400,000 tokens
+ /// - Max Output Tokens: 128,000 tokens + ///
+ public static Model GPT5_1_Codex { get; } = new("gpt-5.1-codex", "openai"); + + /// + /// GPT-5.1 Codex mini is a smaller, more cost-effective, less-capable version of GPT-5.1-Codex. + /// + /// + /// - Context Window: 400,000 tokens
+ /// - Max Output Tokens: 128,000 tokens + ///
+ public static Model GPT5_1_CodexMini { get; } = new("gpt-5.1-codex-mini", "openai"); + /// /// GPT-5-Codex is a version of GPT-5 optimized for agentic coding tasks in Codex or similar environments. /// It's available in the Responses API only and the underlying model snapshot will be regularly updated. diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 83f76e14..8a8da848 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -29,8 +29,15 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx true true - 8.8.7 + 8.8.8 +Version 8.8.8 +- Allow setting Responses.TextContent.Type to OutputText for Role.Assistant messages +- Fixed stream consumed with debug logging enabled +- Fixed wrapped server sent event error object +- Fixed ability to create MCPApprovalResponse for mcp tool approvals +- Fixed MCPToolCall.Error deserialization +- Updated default models Version 8.8.7 - Fix VAD serialization not properly setting disabled values Version 8.8.6 diff --git a/OpenAI-DotNet/Responses/CreateResponseRequest.cs b/OpenAI-DotNet/Responses/CreateResponseRequest.cs index 65bf0abb..306c13d8 100644 --- a/OpenAI-DotNet/Responses/CreateResponseRequest.cs +++ b/OpenAI-DotNet/Responses/CreateResponseRequest.cs @@ -160,7 +160,7 @@ public CreateResponseRequest( { Input = input?.ToArray() ?? throw new ArgumentNullException(nameof(input)); Model = string.IsNullOrWhiteSpace(model?.Id) && prompt == null - ? Models.Model.GPT4oRealtime + ? Models.Model.GPT5_Mini : model; Background = background; Include = include?.ToList(); diff --git a/OpenAI-DotNet/Responses/MCPApprovalResponse.cs b/OpenAI-DotNet/Responses/MCPApprovalResponse.cs index 8dec4567..9afb26fa 100644 --- a/OpenAI-DotNet/Responses/MCPApprovalResponse.cs +++ b/OpenAI-DotNet/Responses/MCPApprovalResponse.cs @@ -6,6 +6,14 @@ namespace OpenAI.Responses { public sealed class MCPApprovalResponse : BaseResponse, IResponseItem { + public MCPApprovalResponse() { } + + public MCPApprovalResponse(string approvalRequestId, bool approve) + { + ApprovalRequestId = approvalRequestId; + Approve = approve; + } + /// [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] @@ -32,12 +40,12 @@ public sealed class MCPApprovalResponse : BaseResponse, IResponseItem [JsonInclude] [JsonPropertyName("approval_request_id")] - public string ApprovalRequestId { get; } + public string ApprovalRequestId { get; private set; } [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] [JsonPropertyName("approve")] - public bool Approve { get; } + public bool Approve { get; private set; } /// /// Optional reason for the decision. @@ -45,6 +53,6 @@ public sealed class MCPApprovalResponse : BaseResponse, IResponseItem [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("reason")] - public string Reason { get; } + public string Reason { get; private set; } } } diff --git a/OpenAI-DotNet/Responses/MCPToolCall.cs b/OpenAI-DotNet/Responses/MCPToolCall.cs index 76367531..5e05eeb3 100644 --- a/OpenAI-DotNet/Responses/MCPToolCall.cs +++ b/OpenAI-DotNet/Responses/MCPToolCall.cs @@ -108,6 +108,6 @@ internal string Delta [JsonInclude] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonPropertyName("error")] - public string Error { get; private set; } + public JsonNode Error { get; private set; } } } diff --git a/OpenAI-DotNet/Responses/Message.cs b/OpenAI-DotNet/Responses/Message.cs index 5e59ef42..26f46898 100644 --- a/OpenAI-DotNet/Responses/Message.cs +++ b/OpenAI-DotNet/Responses/Message.cs @@ -15,7 +15,7 @@ public sealed class Message : BaseResponse, IResponseItem public Message() { } public Message(Role role, string text) - : this(role, new TextContent(text)) + : this(role, new TextContent(text, role == Role.Assistant ? ResponseContentType.OutputText : ResponseContentType.InputText)) { } diff --git a/OpenAI-DotNet/Responses/ResponsesEndpoint.cs b/OpenAI-DotNet/Responses/ResponsesEndpoint.cs index 4e76e2b0..9eca5a10 100644 --- a/OpenAI-DotNet/Responses/ResponsesEndpoint.cs +++ b/OpenAI-DotNet/Responses/ResponsesEndpoint.cs @@ -401,7 +401,8 @@ private async Task StreamResponseAsync(string endpoint, StringContent } case "error": { - serverSentEvent = sseResponse.Deserialize(ssEvent, client); + var error = @object["error"]?.Deserialize(); + serverSentEvent = error ?? sseResponse.Deserialize(ssEvent, client); break; } // Event status messages with no data payloads: diff --git a/OpenAI-DotNet/Responses/TextContent.cs b/OpenAI-DotNet/Responses/TextContent.cs index 49283bbf..8ce42333 100644 --- a/OpenAI-DotNet/Responses/TextContent.cs +++ b/OpenAI-DotNet/Responses/TextContent.cs @@ -13,9 +13,14 @@ public sealed class TextContent : BaseResponse, IResponseContent public TextContent() { } - public TextContent(string text) + public TextContent(string text, ResponseContentType type = ResponseContentType.InputText) { - Type = ResponseContentType.InputText; + if (type != ResponseContentType.InputText && type != ResponseContentType.OutputText) + { + throw new ArgumentException("Invalid response content type. Must be InputText or OutputText.", nameof(type)); + } + + Type = type; Text = text; } diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs index aa2d69cc..2dbf8a84 100644 --- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -479,6 +479,7 @@ private async Task StreamRunAsync(string endpoint, StringContent pa { IServerSentEvent serverSentEvent = null; var @event = ssEvent.Value.GetValue(); + var @object = ssEvent.Value ?? ssEvent.Data; // ReSharper disable AccessToModifiedClosure try @@ -552,7 +553,8 @@ private async Task StreamRunAsync(string endpoint, StringContent pa serverSentEvent = message; break; case "error": - serverSentEvent = sseResponse.Deserialize(ssEvent, client); + var error = @object["error"]?.Deserialize(); + serverSentEvent = error ?? sseResponse.Deserialize(ssEvent, client); break; default: // if not properly handled raise it up to caller to deal with it themselves. diff --git a/README.md b/README.md index f49fd1f1..8f6a0f12 100644 --- a/README.md +++ b/README.md @@ -65,15 +65,15 @@ dotnet add package OpenAI-DotNet - [List Input Items](#list-input-items) - [Cancel Response](#cancel-response) - [Delete Response](#delete-response) -- [Conversations](#conversations) :new: - - [Create Conversation](#create-conversation) :new: - - [Retrieve Conversation](#retrieve-conversation) :new: - - [Update Conversation](#update-conversation) :new: - - [Delete Conversation](#delete-conversation) :new: - - [List Conversation Items](#list-conversation-items) :new: - - [Create Conversation Item](#create-conversation-item) :new: - - [Retrieve Conversation Item](#retrieve-conversation-item) :new: - - [Delete Conversation Item](#delete-conversation-item) :new: +- [Conversations](#conversations) + - [Create Conversation](#create-conversation) + - [Retrieve Conversation](#retrieve-conversation) + - [Update Conversation](#update-conversation) + - [Delete Conversation](#delete-conversation) + - [List Conversation Items](#list-conversation-items) + - [Create Conversation Item](#create-conversation-item) + - [Retrieve Conversation Item](#retrieve-conversation-item) + - [Delete Conversation Item](#delete-conversation-item) - [Realtime](#realtime) - [Create Realtime Session](#create-realtime-session) - [Client Events](#client-events)