diff --git a/src/Custom/Assistants/MessageCreationAttachment.cs b/src/Custom/Assistants/MessageCreationAttachment.cs index 521de9c11..d0de2c392 100644 --- a/src/Custom/Assistants/MessageCreationAttachment.cs +++ b/src/Custom/Assistants/MessageCreationAttachment.cs @@ -26,7 +26,47 @@ public partial class MessageCreationAttachment public IReadOnlyList Tools { get; } private void SerializeTools(Utf8JsonWriter writer, ModelReaderWriterOptions options) - => writer.WriteObjectValue(Tools, options); + { + if (Tools is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + foreach (ToolDefinition tool in Tools) + { + using var ms = new System.IO.MemoryStream(); + using (var tempWriter = new Utf8JsonWriter(ms)) + { + tempWriter.WriteObjectValue(tool, options); + tempWriter.Flush(); + } + + using (JsonDocument doc = JsonDocument.Parse(ms.ToArray())) + { + JsonElement root = doc.RootElement; + if (root.ValueKind == JsonValueKind.Object) + { + writer.WriteStartObject(); + foreach (var prop in root.EnumerateObject()) + { + if (prop.NameEquals("file_search"u8)) + { + continue; + } + prop.WriteTo(writer); + } + writer.WriteEndObject(); + } + else + { + root.WriteTo(writer); + } + } + } + writer.WriteEndArray(); + } private static void DeserializeTools(JsonProperty property, ref IReadOnlyList tools) { diff --git a/tests/Assistants/AssistantsTests.cs b/tests/Assistants/AssistantsTests.cs index 19b785443..cddb2a8b6 100644 --- a/tests/Assistants/AssistantsTests.cs +++ b/tests/Assistants/AssistantsTests.cs @@ -1,10 +1,3 @@ -using Microsoft.ClientModel.TestFramework; -using NUnit.Framework; -using NUnit.Framework.Internal; -using OpenAI.Assistants; -using OpenAI.Files; -using OpenAI.Tests.Utility; -using OpenAI.VectorStores; using System; using System.ClientModel; using System.ClientModel.Primitives; @@ -14,6 +7,12 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.ClientModel.TestFramework; +using NUnit.Framework; +using OpenAI.Assistants; +using OpenAI.Files; +using OpenAI.Tests.Utility; +using OpenAI.VectorStores; using static OpenAI.Tests.TestHelpers; namespace OpenAI.Tests.Assistants; @@ -30,7 +29,6 @@ public class AssistantsTests : OpenAIRecordedTestBase private readonly List _vectorStoreIdsToDelete = []; private static readonly DateTimeOffset s_2024 = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); - private static readonly string s_testAssistantName = $".NET SDK Test Assistant - Please Delete Me"; private static readonly string s_cleanupMetadataKey = $"test_metadata_cleanup_eligible"; private AssistantClient GetTestClient() => GetProxiedOpenAIClient(TestScenario.Assistants); @@ -847,6 +845,51 @@ This file describes the favorite foods of several people. }); } + [RecordedTest] + [LiveOnly] + public async Task FileOnMessageWorks() + { + // First, we need to upload a simple test file. + OpenAIFileClient fileClient = GetTestClient(TestScenario.Files); + OpenAIFile testFile = await fileClient.UploadFileAsync( + BinaryData.FromString(""" + This file describes the favorite foods of several people. + + Summanus Ferdinand: tacos + Tekakwitha Effie: pizza + Filip Carola: cake + """).ToStream(), + "favorite_foods.txt", + FileUploadPurpose.Assistants); + Validate(testFile); + + AssistantClient client = GetTestClient(); + + AssistantThread thread = await client.CreateThreadAsync(); + Validate(thread); + + Assistant assistant = await client.CreateAssistantAsync("gpt-4o-mini"); + Validate(assistant); + + ThreadMessage message = await client.CreateMessageAsync( + thread.Id, + MessageRole.User, + new[] { + MessageContent.FromText("What is this file?"), + }, + new MessageCreationOptions() + { + Attachments = [ + new MessageCreationAttachment(testFile.Id, new List() { ToolDefinition.CreateFileSearch() }), + new MessageCreationAttachment(testFile.Id, new List() { ToolDefinition.CreateCodeInterpreter() }) + ] + } + ); + Validate(message); + + var result = client.CreateRunStreamingAsync(thread.Id, assistant.Id); + } + [RecordedTest] public async Task FileSearchStreamingWorks() { diff --git a/tests/SessionRecords/AssistantsTests/FileOnMessageWorksAsync.json b/tests/SessionRecords/AssistantsTests/FileOnMessageWorksAsync.json new file mode 100644 index 000000000..e6935125f --- /dev/null +++ b/tests/SessionRecords/AssistantsTests/FileOnMessageWorksAsync.json @@ -0,0 +1,192 @@ +{ + "Entries": [ + { + "RequestUri": "https://api.openai.com/v1/threads", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "0", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6d22e3c896b33-DFW", + "Connection": "keep-alive", + "Content-Length": "137", + "Content-Type": "application/json", + "Date": "Sanitized", + "openai-organization": "Sanitized", + "openai-processing-ms": "Sanitized", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Set-Cookie": [ + "Sanitized", + "Sanitized" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "167", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "thread_gAAkyuIXs5PLiQoe8sIEDTXV", + "object": "thread", + "created_at": 1762791037, + "metadata": {}, + "tool_resources": {} + } + }, + { + "RequestUri": "https://api.openai.com/v1/assistants", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "23", + "Content-Type": "application/json", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": { + "model": "gpt-4o-mini" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6d22fae616b33-DFW", + "Connection": "keep-alive", + "Content-Length": "337", + "Content-Type": "application/json", + "Date": "Sanitized", + "openai-organization": "Sanitized", + "openai-processing-ms": "Sanitized", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "664", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "asst_3Gz6lp6RnOg4hCaB8QzgaTAE", + "object": "assistant", + "created_at": 1762791037, + "name": null, + "description": null, + "model": "gpt-4o-mini", + "instructions": null, + "tools": [], + "top_p": 1.0, + "temperature": 1.0, + "reasoning_effort": null, + "tool_resources": {}, + "metadata": {}, + "response_format": "auto" + } + }, + { + "RequestUri": "https://api.openai.com/v1/threads/thread_gAAkyuIXs5PLiQoe8sIEDTXV/messages", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "217", + "Content-Type": "application/json", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": { + "role": "user", + "content": "What is this file?", + "attachments": [ + { + "file_id": "file-LEY8LWxuCKn5TNcydztuW1", + "tools": [ + { + "type": "file_search" + } + ] + }, + { + "file_id": "file-LEY8LWxuCKn5TNcydztuW1", + "tools": [ + { + "type": "code_interpreter" + } + ] + } + ] + }, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6d2347bdd6b33-DFW", + "Connection": "keep-alive", + "Content-Length": "675", + "Content-Type": "application/json", + "Date": "Sanitized", + "openai-organization": "Sanitized", + "openai-processing-ms": "Sanitized", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "2353", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "msg_MZwW4GbZHxjqCfOHGFFuybon", + "object": "thread.message", + "created_at": 1762791039, + "assistant_id": null, + "thread_id": "thread_gAAkyuIXs5PLiQoe8sIEDTXV", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "What is this file?", + "annotations": [] + } + } + ], + "attachments": [ + { + "file_id": "file-LEY8LWxuCKn5TNcydztuW1", + "tools": [ + { + "type": "file_search" + } + ] + }, + { + "file_id": "file-LEY8LWxuCKn5TNcydztuW1", + "tools": [ + { + "type": "code_interpreter" + } + ] + } + ], + "metadata": {} + } + } + ], + "Variables": { + "OPEN-API-KEY": "api-key" + } +} diff --git a/tests/SessionRecords/AssistantsTests/oldRec.json b/tests/SessionRecords/AssistantsTests/oldRec.json new file mode 100644 index 000000000..14201f102 --- /dev/null +++ b/tests/SessionRecords/AssistantsTests/oldRec.json @@ -0,0 +1,192 @@ +{ + "Entries": [ + { + "RequestUri": "https://api.openai.com/v1/threads", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "0", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6aca838f32e7e-DFW", + "Connection": "keep-alive", + "Content-Length": "137", + "Content-Type": "application/json", + "Date": "Mon, 10 Nov 2025 15:45:00 GMT", + "openai-organization": "Sanitized", + "openai-processing-ms": "148", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Set-Cookie": [ + "Sanitized", + "Sanitized" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "152", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "thread_mlsy3q65fEzDnKL36aB4Tpnt", + "object": "thread", + "created_at": 1762789500, + "metadata": {}, + "tool_resources": {} + } + }, + { + "RequestUri": "https://api.openai.com/v1/assistants", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "23", + "Content-Type": "application/json", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": { + "model": "gpt-4o-mini" + }, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6aca98a942e7e-DFW", + "Connection": "keep-alive", + "Content-Length": "337", + "Content-Type": "application/json", + "Date": "Mon, 10 Nov 2025 15:45:00 GMT", + "openai-organization": "Sanitized", + "openai-processing-ms": "334", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "340", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "asst_iQg1VLcm2mQGzgsWskfieYAr", + "object": "assistant", + "created_at": 1762789500, + "name": null, + "description": null, + "model": "gpt-4o-mini", + "instructions": null, + "tools": [], + "top_p": 1.0, + "temperature": 1.0, + "reasoning_effort": null, + "tool_resources": {}, + "metadata": {}, + "response_format": "auto" + } + }, + { + "RequestUri": "https://api.openai.com/v1/threads/thread_mlsy3q65fEzDnKL36aB4Tpnt/messages", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept": "application/json", + "Authorization": "Sanitized", + "Content-Length": "217", + "Content-Type": "application/json", + "OpenAI-Beta": "assistants=v2", + "User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)" + }, + "RequestBody": { + "role": "user", + "content": "What is this file?", + "attachments": [ + { + "file_id": "file-2UNNQbG4wyuwcLNCy8cHXR", + "tools": [ + { + "type": "file_search" + } + ] + }, + { + "file_id": "file-2UNNQbG4wyuwcLNCy8cHXR", + "tools": [ + { + "type": "code_interpreter" + } + ] + } + ] + }, + "StatusCode": 200, + "ResponseHeaders": { + "Alt-Svc": "h3=\":443\"", + "cf-cache-status": "DYNAMIC", + "CF-RAY": "99c6acacce8d2e7e-DFW", + "Connection": "keep-alive", + "Content-Length": "675", + "Content-Type": "application/json", + "Date": "Mon, 10 Nov 2025 15:45:03 GMT", + "openai-organization": "Sanitized", + "openai-processing-ms": "2701", + "openai-project": "Sanitized", + "openai-version": "2020-10-01", + "Server": "cloudflare", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "X-Content-Type-Options": "nosniff", + "x-envoy-upstream-service-time": "2708", + "x-openai-proxy-wasm": "v0.1", + "X-Request-ID": "Sanitized" + }, + "ResponseBody": { + "id": "msg_KyUysRKwOtwl7yHi2LXBQFOk", + "object": "thread.message", + "created_at": 1762789503, + "assistant_id": null, + "thread_id": "thread_mlsy3q65fEzDnKL36aB4Tpnt", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "What is this file?", + "annotations": [] + } + } + ], + "attachments": [ + { + "file_id": "file-2UNNQbG4wyuwcLNCy8cHXR", + "tools": [ + { + "type": "file_search" + } + ] + }, + { + "file_id": "file-2UNNQbG4wyuwcLNCy8cHXR", + "tools": [ + { + "type": "code_interpreter" + } + ] + } + ], + "metadata": {} + } + } + ], + "Variables": { + "OPEN-API-KEY": "api-key" + } +} diff --git a/tests/Utility/OpenAIRecordedTestBase.cs b/tests/Utility/OpenAIRecordedTestBase.cs index 6d97e302f..97fc8d813 100644 --- a/tests/Utility/OpenAIRecordedTestBase.cs +++ b/tests/Utility/OpenAIRecordedTestBase.cs @@ -1,4 +1,5 @@ using Microsoft.ClientModel.TestFramework; +using Microsoft.ClientModel.TestFramework.TestProxy.Admin; using NUnit.Framework; using System.ClientModel; using static OpenAI.Tests.TestHelpers; @@ -13,6 +14,8 @@ public OpenAIRecordedTestBase(bool isAsync, RecordedTestMode? mode = null) : bas SanitizedHeaders.Add("openai-organization"); SanitizedHeaders.Add("openai-project"); SanitizedHeaders.Add("X-Request-ID"); + SanitizedHeaders.Add("openai-processing-ms"); + SanitizedHeaders.Add("Date"); JsonPathSanitizers.Add("$.system_fingerprint"); }