.NET: Improve fidelity of OpenAI ChatCompletions Hosting (#1785)

* rename, support json serialization

* wip

* non-streaming

* streaming?

* proper streaming types

* comments + fix audio parse

* copilot suggestions

* proper stopsequences type

* build options as i could

* annotations

* proper generation of Id for chatcompletions

* string length as in chatcompletions api ref

* image url

* support tools

* rework API

* introduce tests for chatcompletions

* function calling / serialization tests / fixes

* more tests and coverage

* fix format

* sort usings

* nit

* address PR comments

* nits
This commit is contained in:
Korolev Dmitry
2025-11-05 09:55:26 +00:00
committed by GitHub
parent 54db13c22f
commit bb8ef466de
46 changed files with 5687 additions and 406 deletions
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
@@ -22,16 +23,19 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Tests;
/// </summary>
public abstract class ConformanceTestBase : IAsyncDisposable
{
protected const string TracesBasePath = "ConformanceTraces/Responses";
protected const string TracesBasePath = "ConformanceTraces";
protected const string ResponsesTracesDirectory = "Responses";
protected const string ChatCompletionsTracesDirectory = "ChatCompletions";
private WebApplication? _app;
private HttpClient? _httpClient;
/// <summary>
/// Loads a JSON file from the conformance traces directory.
/// </summary>
protected static string LoadTraceFile(string relativePath)
protected static string LoadTraceFile(string directory, string relativePath)
{
var fullPath = Path.Combine(TracesBasePath, relativePath);
var fullPath = Path.Combine(TracesBasePath, directory, relativePath);
if (!File.Exists(fullPath))
{
@@ -41,12 +45,33 @@ public abstract class ConformanceTestBase : IAsyncDisposable
return File.ReadAllText(fullPath);
}
/// <summary>
/// Loads a JSON file from the conformance traces directory.
/// </summary>
protected static string LoadResponsesTraceFile(string relativePath)
=> LoadTraceFile(ResponsesTracesDirectory, relativePath);
/// <summary>
/// Loads a JSON document from the conformance traces directory.
/// </summary>
protected static JsonDocument LoadTraceDocument(string relativePath)
protected static JsonDocument LoadResponsesTraceDocument(string relativePath)
{
var json = LoadTraceFile(relativePath);
var json = LoadResponsesTraceFile(relativePath);
return JsonDocument.Parse(json);
}
/// <summary>
/// Loads a JSON file from the conformance traces directory.
/// </summary>
protected static string LoadChatCompletionsTraceFile(string relativePath)
=> LoadTraceFile(ChatCompletionsTracesDirectory, relativePath);
/// <summary>
/// Loads a JSON document from the conformance traces directory.
/// </summary>
protected static JsonDocument LoadChatCompletionsTraceDocument(string relativePath)
{
var json = LoadChatCompletionsTraceFile(relativePath);
return JsonDocument.Parse(json);
}
@@ -61,6 +86,20 @@ public abstract class ConformanceTestBase : IAsyncDisposable
}
}
/// <summary>
/// Asserts that a JSON element has any of the passed string values.
/// </summary>
protected static void AssertJsonPropertyEquals(JsonElement element, string propertyName, params string[] anyOfValues)
{
AssertJsonPropertyExists(element, propertyName);
var actualValue = element.GetProperty(propertyName).GetString();
if (!anyOfValues.Contains(actualValue))
{
throw new Xunit.Sdk.XunitException($"Property '{propertyName}': expected any of '{string.Join("; ", anyOfValues)}', got '{actualValue}'");
}
}
/// <summary>
/// Asserts that a JSON element has a specific string value.
/// </summary>
@@ -75,6 +114,20 @@ public abstract class ConformanceTestBase : IAsyncDisposable
}
}
/// <summary>
/// Asserts that a JSON element has a specific string value.
/// </summary>
protected static void AssertJsonPropertyEquals(JsonElement element, string propertyName, float expectedValue)
{
AssertJsonPropertyExists(element, propertyName);
var actualValue = element.GetProperty(propertyName).GetDouble();
if (actualValue != expectedValue)
{
throw new Xunit.Sdk.XunitException($"Property '{propertyName}': expected '{expectedValue}', got '{actualValue}'");
}
}
/// <summary>
/// Asserts that a JSON element has a specific integer value.
/// </summary>
@@ -141,10 +194,12 @@ public abstract class ConformanceTestBase : IAsyncDisposable
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
@@ -171,10 +226,12 @@ public abstract class ConformanceTestBase : IAsyncDisposable
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
@@ -188,12 +245,21 @@ public abstract class ConformanceTestBase : IAsyncDisposable
/// <summary>
/// Sends a POST request with JSON content to the test server.
/// </summary>
protected async Task<HttpResponseMessage> SendRequestAsync(HttpClient client, string agentName, string requestJson)
protected async Task<HttpResponseMessage> SendResponsesRequestAsync(HttpClient client, string agentName, string requestJson)
{
StringContent content = new(requestJson, Encoding.UTF8, "application/json");
return await client.PostAsync(new Uri($"/{agentName}/v1/responses", UriKind.Relative), content);
}
/// <summary>
/// Sends a POST request with JSON content to the test server.
/// </summary>
protected async Task<HttpResponseMessage> SendChatCompletionRequestAsync(HttpClient client, string agentName, string requestJson)
{
StringContent content = new(requestJson, Encoding.UTF8, "application/json");
return await client.PostAsync(new Uri($"/{agentName}/v1/chat/completions", UriKind.Relative), content);
}
/// <summary>
/// Parses the response JSON and returns a JsonDocument.
/// </summary>
@@ -0,0 +1,12 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Hello, how are you?"
}
],
"max_completion_tokens": 100,
"temperature": 1.0,
"top_p": 1.0
}
@@ -0,0 +1,33 @@
{
"id": "chatcmpl-AaBbCcDdEeFfGg",
"object": "chat.completion",
"created": 1730371200,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! I'm doing well, thank you. How about you?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 13,
"completion_tokens": 14,
"total_tokens": 27,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_1234567890"
}
@@ -0,0 +1,34 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "What's the weather in San Francisco?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": [ "celsius", "fahrenheit" ],
"description": "The unit of temperature"
}
},
"required": [ "location" ]
}
}
}
],
"tool_choice": "auto"
}
@@ -0,0 +1,43 @@
{
"id": "chatcmpl-DEF456",
"object": "chat.completion",
"created": 1730371250,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123xyz",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"fahrenheit\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 85,
"completion_tokens": 18,
"total_tokens": 103,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_1234567890"
}
@@ -0,0 +1,36 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant that outputs JSON."
},
{
"role": "user",
"content": "Provide information about a person named John Doe, age 30, who is a software engineer."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person_info",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "number"
},
"occupation": {
"type": "string"
}
},
"required": [ "name", "age", "occupation" ],
"additionalProperties": false
}
}
}
}
@@ -0,0 +1,33 @@
{
"id": "chatcmpl-MNO345",
"object": "chat.completion",
"created": 1730371400,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"name\":\"John Doe\",\"age\":30,\"occupation\":\"software engineer\"}"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 45,
"completion_tokens": 18,
"total_tokens": 63,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_5544332211"
}
@@ -0,0 +1,18 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "What is 2+2?"
},
{
"role": "assistant",
"content": "2+2 equals 4."
},
{
"role": "user",
"content": "What about 3+3?"
}
],
"max_completion_tokens": 50
}
@@ -0,0 +1,33 @@
{
"id": "chatcmpl-JKL012",
"object": "chat.completion",
"created": 1730371350,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "3+3 equals 6."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 35,
"completion_tokens": 8,
"total_tokens": 43,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_1122334455"
}
@@ -0,0 +1,12 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Write a short poem about AI."
}
],
"max_completion_tokens": 150,
"temperature": 1.0,
"stream": true
}
@@ -0,0 +1,21 @@
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":"In"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":" circuits"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":" bright"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":","},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":" minds"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":" take"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":" flight"},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{"content":"."},"finish_reason":null}]}
data: {"id":"chatcmpl-ABC123","object":"chat.completion.chunk","created":1730371200,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":12,"total_tokens":24,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
data: [DONE]
@@ -0,0 +1,14 @@
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant that speaks like a pirate."
},
{
"role": "user",
"content": "Tell me about the ocean."
}
],
"max_completion_tokens": 100
}
@@ -0,0 +1,33 @@
{
"id": "chatcmpl-GHI789",
"object": "chat.completion",
"created": 1730371300,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Ahoy, matey! The ocean be a vast, mysterious realm full of treasures and creatures!"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 28,
"completion_tokens": 20,
"total_tokens": 48,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_9876543210"
}
@@ -36,7 +36,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
]);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -76,7 +76,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
]);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -111,7 +111,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
]);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -144,7 +144,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, ErrorMessage);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -179,7 +179,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, ErrorMessage);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -213,7 +213,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, "Error message");
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -240,7 +240,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateImageContentAgentAsync(AgentName, ImageUrl, isDataUri: false);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -264,7 +264,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateImageContentAgentAsync(AgentName, DataUri, isDataUri: true);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -289,7 +289,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateImageContentWithDetailAgentAsync(AgentName, ImageUrl, Detail);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -313,7 +313,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateImageContentAgentAsync(AgentName, "https://example.com/test.png", isDataUri: false);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -339,7 +339,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, "audio/mpeg");
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -364,7 +364,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, "audio/wav");
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -390,7 +390,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, mediaType);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -415,7 +415,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateHostedFileContentAgentAsync(AgentName, FileId);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -438,7 +438,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateHostedFileContentAgentAsync(AgentName, "file-xyz789");
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -465,7 +465,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateFileContentAgentAsync(AgentName, FileDataUri, Filename);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -490,7 +490,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateFileContentAgentAsync(AgentName, FileDataUri, null);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -516,7 +516,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateMixedContentAgentAsync(AgentName);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -535,7 +535,7 @@ public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
HttpClient client = await this.CreateErrorAndTextContentAgentAsync(AgentName);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
@@ -27,4 +27,16 @@
</None>
</ItemGroup>
<ItemGroup>
<Content Remove="ConformanceTraces\ChatCompletions\function_calling\request.json" />
<Content Remove="ConformanceTraces\ChatCompletions\function_calling\response.json" />
<Content Remove="ConformanceTraces\ChatCompletions\json_mode\request.json" />
<Content Remove="ConformanceTraces\ChatCompletions\json_mode\response.json" />
<Content Remove="ConformanceTraces\ChatCompletions\multi_turn\request.json" />
<Content Remove="ConformanceTraces\ChatCompletions\multi_turn\response.json" />
<Content Remove="ConformanceTraces\ChatCompletions\streaming\request.json" />
<Content Remove="ConformanceTraces\ChatCompletions\system_message\request.json" />
<Content Remove="ConformanceTraces\ChatCompletions\system_message\response.json" />
</ItemGroup>
</Project>
@@ -0,0 +1,495 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Tests;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Conformance tests for OpenAI Chat Completions API implementation behavior.
/// Tests use real API traces to ensure our implementation produces responses
/// that match OpenAI's wire format when processing actual requests through the server.
/// </summary>
public sealed class OpenAIChatCompletionsConformanceTests : ConformanceTestBase
{
[Fact]
public async Task BasicRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("basic/request.json");
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("basic/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get the expected response text from the trace to use as mock response
string expectedText = expectedResponse.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content").GetString()!;
HttpClient client = await this.CreateTestServerAsync("basic-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "basic-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Parse the request to verify it was sent correctly
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Verify request was properly formatted (structure check)
AssertJsonPropertyEquals(request, "model", "gpt-4o-mini");
AssertJsonPropertyExists(request, "messages");
AssertJsonPropertyEquals(request, "max_completion_tokens", 100);
AssertJsonPropertyEquals(request, "temperature", 1.0f);
AssertJsonPropertyEquals(request, "top_p", 1.0f);
var messages = request.GetProperty("messages");
Assert.Equal(JsonValueKind.Array, messages.ValueKind);
Assert.True(messages.GetArrayLength() > 0, "Messages array should not be empty");
var firstMessage = messages[0];
AssertJsonPropertyEquals(firstMessage, "role", "user");
AssertJsonPropertyEquals(firstMessage, "content", "Hello, how are you?");
// Assert - Response metadata (IDs and timestamps are dynamic, just verify structure)
AssertJsonPropertyExists(response, "id");
AssertJsonPropertyEquals(response, "object", "chat.completion");
AssertJsonPropertyExists(response, "created");
AssertJsonPropertyExists(response, "model");
var id = response.GetProperty("id").GetString();
Assert.NotNull(id);
Assert.StartsWith("chatcmpl-", id);
var createdAt = response.GetProperty("created").GetInt64();
Assert.True(createdAt > 0, "created should be a positive unix timestamp");
var model = response.GetProperty("model").GetString();
Assert.NotNull(model);
Assert.StartsWith("gpt-4o-mini", model);
// Assert - Choices array structure
AssertJsonPropertyExists(response, "choices");
var choices = response.GetProperty("choices");
Assert.Equal(JsonValueKind.Array, choices.ValueKind);
Assert.True(choices.GetArrayLength() > 0, "Choices array should not be empty");
// Assert - Choice structure
var firstChoice = choices[0];
AssertJsonPropertyExists(firstChoice, "index");
AssertJsonPropertyEquals(firstChoice, "index", 0);
AssertJsonPropertyExists(firstChoice, "message");
AssertJsonPropertyExists(firstChoice, "finish_reason");
var finishReason = firstChoice.GetProperty("finish_reason").GetString();
Assert.NotNull(finishReason);
Assert.Contains(finishReason, collection: ["stop", "length", "content_filter", "tool_calls"]);
// Assert - Message structure
var message = firstChoice.GetProperty("message");
AssertJsonPropertyExists(message, "role");
AssertJsonPropertyEquals(message, "role", "assistant");
AssertJsonPropertyExists(message, "content");
var content = message.GetProperty("content").GetString();
Assert.NotNull(content);
Assert.Equal(expectedText, content); // Verify actual content matches expected
// Assert - Usage statistics
AssertJsonPropertyExists(response, "usage");
var usage = response.GetProperty("usage");
AssertJsonPropertyExists(usage, "prompt_tokens");
AssertJsonPropertyExists(usage, "completion_tokens");
AssertJsonPropertyExists(usage, "total_tokens");
var promptTokens = usage.GetProperty("prompt_tokens").GetInt32();
var completionTokens = usage.GetProperty("completion_tokens").GetInt32();
var totalTokens = usage.GetProperty("total_tokens").GetInt32();
Assert.True(promptTokens > 0, "prompt_tokens should be positive");
Assert.True(completionTokens > 0, "completion_tokens should be positive");
Assert.Equal(promptTokens + completionTokens, totalTokens);
// Assert - Usage details
AssertJsonPropertyExists(usage, "prompt_tokens_details");
var promptDetails = usage.GetProperty("prompt_tokens_details");
AssertJsonPropertyExists(promptDetails, "cached_tokens");
AssertJsonPropertyExists(promptDetails, "audio_tokens");
Assert.True(promptDetails.GetProperty("cached_tokens").GetInt32() >= 0);
Assert.True(promptDetails.GetProperty("audio_tokens").GetInt32() >= 0);
AssertJsonPropertyExists(usage, "completion_tokens_details");
var completionDetails = usage.GetProperty("completion_tokens_details");
AssertJsonPropertyExists(completionDetails, "reasoning_tokens");
AssertJsonPropertyExists(completionDetails, "audio_tokens");
AssertJsonPropertyExists(completionDetails, "accepted_prediction_tokens");
AssertJsonPropertyExists(completionDetails, "rejected_prediction_tokens");
Assert.True(completionDetails.GetProperty("reasoning_tokens").GetInt32() >= 0);
Assert.True(completionDetails.GetProperty("audio_tokens").GetInt32() >= 0);
Assert.True(completionDetails.GetProperty("accepted_prediction_tokens").GetInt32() >= 0);
Assert.True(completionDetails.GetProperty("rejected_prediction_tokens").GetInt32() >= 0);
// Assert - Optional fields
AssertJsonPropertyExists(response, "service_tier");
var serviceTier = response.GetProperty("service_tier").GetString();
Assert.NotNull(serviceTier);
Assert.True(serviceTier == "default" || serviceTier == "auto", $"service_tier should be 'default' or 'auto', got '{serviceTier}'");
}
[Fact]
public async Task StreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("streaming/request.json");
string expectedResponseSse = LoadChatCompletionsTraceFile("streaming/response.txt");
// Extract expected text from SSE chunks
var expectedChunks = ParseChatCompletionChunksFromSse(expectedResponseSse);
string expectedText = string.Concat(expectedChunks
.Where(c => c.GetProperty("choices")[0].GetProperty("delta").TryGetProperty("content", out var content))
.Select(c => c.GetProperty("choices")[0].GetProperty("delta").GetProperty("content").GetString()));
HttpClient client = await this.CreateTestServerAsync("streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var chunks = ParseChatCompletionChunksFromSse(responseSse);
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has stream flag
AssertJsonPropertyEquals(request, "stream", true);
// Assert - Response has valid chunks
Assert.NotEmpty(chunks);
// Assert - All chunks have same ID
string? firstId = null;
foreach (var chunk in chunks)
{
AssertJsonPropertyExists(chunk, "id");
AssertJsonPropertyEquals(chunk, "object", "chat.completion.chunk");
AssertJsonPropertyExists(chunk, "created");
AssertJsonPropertyExists(chunk, "model");
AssertJsonPropertyExists(chunk, "choices");
string chunkId = chunk.GetProperty("id").GetString()!;
Assert.StartsWith("chatcmpl-", chunkId);
firstId ??= chunkId;
Assert.Equal(firstId, chunkId);
}
// Assert - First chunk has role
var firstChunk = chunks[0];
var firstChoice = firstChunk.GetProperty("choices")[0];
AssertJsonPropertyExists(firstChoice, "delta");
var firstDelta = firstChoice.GetProperty("delta");
if (firstDelta.TryGetProperty("role", out var role))
{
Assert.Equal("assistant", role.GetString());
}
// Assert - Content chunks have delta content
var contentChunks = chunks.Where(c =>
c.GetProperty("choices")[0].GetProperty("delta").TryGetProperty("content", out _)).ToList();
Assert.NotEmpty(contentChunks);
// Assert - Last chunk has finish_reason
var lastChunk = chunks[^1];
var lastChoice = lastChunk.GetProperty("choices")[0];
if (lastChoice.TryGetProperty("finish_reason", out var finishReason) && finishReason.ValueKind != JsonValueKind.Null)
{
string reason = finishReason.GetString()!;
Assert.Contains(reason, collection: ["stop", "length", "tool_calls", "content_filter"]);
}
// Assert - Last chunk may have usage
if (lastChunk.TryGetProperty("usage", out var usage))
{
AssertJsonPropertyExists(usage, "prompt_tokens");
AssertJsonPropertyExists(usage, "completion_tokens");
AssertJsonPropertyExists(usage, "total_tokens");
}
// Assert - Accumulated content matches expected
string accumulatedText = string.Concat(contentChunks
.Select(c => c.GetProperty("choices")[0].GetProperty("delta").GetProperty("content").GetString()));
Assert.NotEmpty(accumulatedText);
}
[Fact]
public async Task FunctionCallingRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("function_calling/request.json");
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("function_calling/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected function call details
const string FunctionName = "get_weather";
HttpClient client = await this.CreateTestServerAsync("function-agent", "You are a helpful assistant.", FunctionName,
(msg) => [new FunctionCallContent("call_abc123xyz", "get_weather", new Dictionary<string, object?>() {
{ "location", "San Francisco, CA" },
{ "unit", "fahrenheit" }
})]
);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "function-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has tools array
AssertJsonPropertyExists(request, "tools");
var tools = request.GetProperty("tools");
Assert.Equal(JsonValueKind.Array, tools.ValueKind);
Assert.True(tools.GetArrayLength() > 0);
// Assert - Tool structure
var tool = tools[0];
AssertJsonPropertyEquals(tool, "type", "function");
AssertJsonPropertyExists(tool, "function");
var function = tool.GetProperty("function");
AssertJsonPropertyEquals(function, "name", "get_weather");
AssertJsonPropertyExists(function, "description");
AssertJsonPropertyExists(function, "parameters");
// Assert - Parameters have JSON Schema
var parameters = function.GetProperty("parameters");
AssertJsonPropertyEquals(parameters, "type", "object");
AssertJsonPropertyExists(parameters, "properties");
AssertJsonPropertyExists(parameters, "required");
// Assert - Response has tool_calls. Not always will return that, so can default to "stop"
var choices = response.GetProperty("choices");
var choice = choices[0];
var message = choice.GetProperty("message");
AssertJsonPropertyEquals(choice, "finish_reason", ["tool_calls", "stop"]);
AssertJsonPropertyExists(message, "tool_calls");
// Assert - Tool call structure
var toolCalls = message.GetProperty("tool_calls");
Assert.Equal(JsonValueKind.Array, toolCalls.ValueKind);
Assert.True(toolCalls.GetArrayLength() > 0);
var toolCall = toolCalls[0];
AssertJsonPropertyExists(toolCall, "id");
AssertJsonPropertyEquals(toolCall, "type", "function");
AssertJsonPropertyExists(toolCall, "function");
var callFunction = toolCall.GetProperty("function");
AssertJsonPropertyEquals(callFunction, "name", "get_weather");
AssertJsonPropertyExists(callFunction, "arguments");
// Assert - Arguments are valid JSON
string arguments = callFunction.GetProperty("arguments").GetString()!;
using var argsDoc = JsonDocument.Parse(arguments);
var argsRoot = argsDoc.RootElement;
AssertJsonPropertyExists(argsRoot, "location");
// Assert - Message content is null when tool_calls present. Can be absent or null.
if (message.TryGetProperty("content", out var contentProp))
{
Assert.Equal(JsonValueKind.Null, contentProp.ValueKind);
}
}
[Fact]
public async Task SystemMessageRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("system_message/request.json");
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("system_message/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
string expectedText = expectedResponse.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content").GetString()!;
HttpClient client = await this.CreateTestServerAsync("system-agent", "You are a helpful assistant that speaks like a pirate.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "system-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has messages with system role
var messages = request.GetProperty("messages");
Assert.True(messages.GetArrayLength() >= 2);
var systemMessage = messages[0];
AssertJsonPropertyEquals(systemMessage, "role", "system");
AssertJsonPropertyExists(systemMessage, "content");
string systemContent = systemMessage.GetProperty("content").GetString()!;
Assert.Contains("pirate", systemContent, System.StringComparison.OrdinalIgnoreCase);
var userMessage = messages[1];
AssertJsonPropertyEquals(userMessage, "role", "user");
// Assert - Response reflects system message influence
var responseMessage = response.GetProperty("choices")[0].GetProperty("message");
string content = responseMessage.GetProperty("content").GetString()!;
Assert.NotNull(content);
Assert.Equal(expectedText, content);
}
[Fact]
public async Task MultiTurnConversationRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("multi_turn/request.json");
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("multi_turn/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
string expectedText = expectedResponse.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content").GetString()!;
HttpClient client = await this.CreateTestServerAsync("multi-turn-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "multi-turn-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has conversation history
var messages = request.GetProperty("messages");
Assert.True(messages.GetArrayLength() >= 3, "Should have at least 3 messages for multi-turn");
// Assert - Message sequence alternates between user and assistant
AssertJsonPropertyEquals(messages[0], "role", "user");
AssertJsonPropertyEquals(messages[1], "role", "assistant");
AssertJsonPropertyEquals(messages[2], "role", "user");
// Assert - Response continues conversation
var responseMessage = response.GetProperty("choices")[0].GetProperty("message");
AssertJsonPropertyEquals(responseMessage, "role", "assistant");
string content = responseMessage.GetProperty("content").GetString()!;
Assert.NotNull(content);
Assert.Equal(expectedText, content);
// Assert - Usage tokens account for conversation history
var usage = response.GetProperty("usage");
int promptTokens = usage.GetProperty("prompt_tokens").GetInt32();
Assert.True(promptTokens > 20, "Prompt tokens should account for conversation history");
}
[Fact]
public async Task JsonModeRequestResponseAsync()
{
// Arrange
string requestJson = LoadChatCompletionsTraceFile("json_mode/request.json");
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("json_mode/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
string expectedText = expectedResponse.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content").GetString()!;
HttpClient client = await this.CreateTestServerAsync("json-agent", "You are a helpful assistant that outputs JSON.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "json-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has response_format with json_schema
AssertJsonPropertyExists(request, "response_format");
var responseFormat = request.GetProperty("response_format");
AssertJsonPropertyEquals(responseFormat, "type", "json_schema");
AssertJsonPropertyExists(responseFormat, "json_schema");
var jsonSchema = responseFormat.GetProperty("json_schema");
AssertJsonPropertyEquals(jsonSchema, "name", "person_info");
AssertJsonPropertyEquals(jsonSchema, "strict", true);
AssertJsonPropertyExists(jsonSchema, "schema");
var schema = jsonSchema.GetProperty("schema");
AssertJsonPropertyEquals(schema, "type", "object");
AssertJsonPropertyExists(schema, "properties");
AssertJsonPropertyExists(schema, "required");
// Assert - Response content is valid JSON matching schema
var responseMessage = response.GetProperty("choices")[0].GetProperty("message");
string content = responseMessage.GetProperty("content").GetString()!;
Assert.NotNull(content);
Assert.Equal(expectedText, content);
using var jsonDoc = JsonDocument.Parse(content);
var jsonRoot = jsonDoc.RootElement;
AssertJsonPropertyExists(jsonRoot, "name");
AssertJsonPropertyExists(jsonRoot, "age");
AssertJsonPropertyExists(jsonRoot, "occupation");
Assert.Equal(JsonValueKind.String, jsonRoot.GetProperty("name").ValueKind);
Assert.Equal(JsonValueKind.Number, jsonRoot.GetProperty("age").ValueKind);
Assert.Equal(JsonValueKind.String, jsonRoot.GetProperty("occupation").ValueKind);
}
/// <summary>
/// Helper to parse chat completion chunks from SSE response.
/// </summary>
private static List<JsonElement> ParseChatCompletionChunksFromSse(string sseContent)
{
var chunks = new List<JsonElement>();
var lines = sseContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].TrimEnd('\r');
if (line.StartsWith("data: ", System.StringComparison.Ordinal))
{
var jsonData = line.Substring("data: ".Length);
// Skip [DONE] marker
if (jsonData == "[DONE]")
{
continue;
}
try
{
var doc = JsonDocument.Parse(jsonData);
chunks.Add(doc.RootElement.Clone());
}
catch
{
// Skip invalid JSON
}
}
}
return chunks;
}
}
@@ -0,0 +1,974 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenAI;
using OpenAI.Chat;
using ChatFinishReason = OpenAI.Chat.ChatFinishReason;
using ChatMessage = OpenAI.Chat.ChatMessage;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Integration tests that start a web server and use the OpenAI Chat Completions SDK client to verify protocol compatibility.
/// These tests validate both streaming and non-streaming request scenarios.
/// </summary>
public sealed class OpenAIChatCompletionsIntegrationTests : IAsyncDisposable
{
private WebApplication? _app;
private HttpClient? _httpClient;
public async ValueTask DisposeAsync()
{
this._httpClient?.Dispose();
if (this._app != null)
{
await this._app.DisposeAsync();
}
}
/// <summary>
/// Verifies that streaming chat completions work correctly with the OpenAI SDK client.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_WithSimpleMessage_ReturnsStreamingUpdatesAsync()
{
// Arrange
const string AgentName = "streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "One Two Three";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Count to 3")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
List<StreamingChatCompletionUpdate> updates = [];
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
updates.Add(update);
if (update.ContentUpdate.Count > 0)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
}
Assert.NotEmpty(updates);
// Verify content was received
string content = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, content);
// Verify finish reason
StreamingChatCompletionUpdate? lastUpdate = updates.LastOrDefault(u => u.FinishReason != null);
Assert.NotNull(lastUpdate);
Assert.Equal(ChatFinishReason.Stop, lastUpdate.FinishReason);
}
/// <summary>
/// Verifies that non-streaming chat completions work correctly with the OpenAI SDK client.
/// </summary>
[Fact]
public async Task CreateChatCompletion_WithSimpleMessage_ReturnsCompleteResponseAsync()
{
// Arrange
const string AgentName = "non-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello! How can I help you today?";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Hello")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion);
Assert.NotNull(completion.Id);
Assert.StartsWith("chatcmpl-", completion.Id);
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
// Verify content
string content = completion.Content[0].Text;
Assert.Equal(ExpectedResponse, content);
}
/// <summary>
/// Verifies that streaming chat completions can handle multiple content chunks.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_WithMultipleChunks_StreamsAllContentAsync()
{
// Arrange
const string AgentName = "multi-chunk-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "This is a test response with multiple words";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
List<StreamingChatCompletionUpdate> updates = [];
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
updates.Add(update);
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
// Verify all content was received
string receivedContent = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, receivedContent);
// Verify multiple content chunks were received
List<StreamingChatCompletionUpdate> contentUpdates = updates.Where(u => u.ContentUpdate.Count > 0).ToList();
Assert.True(contentUpdates.Count > 1, "Expected multiple content chunks in streaming response");
}
/// <summary>
/// Verifies that multiple agents can be accessed via the same server.
/// </summary>
[Fact]
public async Task CreateChatCompletion_WithMultipleAgents_EachAgentRespondsCorrectlyAsync()
{
// Arrange
const string Agent1Name = "agent-one";
const string Agent1Instructions = "You are agent one.";
const string Agent1Response = "Response from agent one";
const string Agent2Name = "agent-two";
const string Agent2Instructions = "You are agent two.";
const string Agent2Response = "Response from agent two";
this._httpClient = await this.CreateTestServerWithMultipleAgentsAsync(
(Agent1Name, Agent1Instructions, Agent1Response),
(Agent2Name, Agent2Instructions, Agent2Response));
ChatClient chatClient1 = this.CreateChatClient(Agent1Name);
ChatClient chatClient2 = this.CreateChatClient(Agent2Name);
List<ChatMessage> messages =
[
new UserChatMessage("Hello")
];
// Act
ChatCompletion completion1 = await chatClient1.CompleteChatAsync(messages);
ChatCompletion completion2 = await chatClient2.CompleteChatAsync(messages);
// Assert
string content1 = completion1.Content[0].Text;
string content2 = completion2.Content[0].Text;
Assert.Equal(Agent1Response, content1);
Assert.Equal(Agent2Response, content2);
Assert.NotEqual(content1, content2);
}
/// <summary>
/// Verifies that streaming and non-streaming work correctly for the same agent.
/// </summary>
[Fact]
public async Task CreateChatCompletion_SameAgentStreamingAndNonStreaming_BothWorkCorrectlyAsync()
{
// Arrange
const string AgentName = "dual-mode-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "This is the response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act - Non-streaming
ChatCompletion nonStreamingCompletion = await chatClient.CompleteChatAsync(messages);
// Act - Streaming
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
StringBuilder streamingContent = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
streamingContent.Append(contentPart.Text);
}
}
// Assert
string nonStreamingContent = nonStreamingCompletion.Content[0].Text;
Assert.Equal(ExpectedResponse, nonStreamingContent);
Assert.Equal(ExpectedResponse, streamingContent.ToString());
}
/// <summary>
/// Verifies that the finish reason is correctly set for completed responses.
/// </summary>
[Fact]
public async Task CreateChatCompletion_CompletedResponse_HasCorrectFinishReasonAsync()
{
// Arrange
const string AgentName = "finish-reason-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Complete";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
Assert.NotNull(completion.Id);
Assert.Equal(ExpectedResponse, completion.Content[0].Text);
}
/// <summary>
/// Verifies that streaming responses contain the expected chunk sequence.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_VerifyChunkSequence_ContainsExpectedDataAsync()
{
// Arrange
const string AgentName = "chunk-sequence-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test response with multiple words";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
List<StreamingChatCompletionUpdate> updates = [];
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
updates.Add(update);
}
// Verify chunks received
Assert.NotEmpty(updates);
// First chunk should have role
StreamingChatCompletionUpdate? firstUpdate = updates.FirstOrDefault(u => u.Role != null);
if (firstUpdate != null)
{
Assert.Equal(ChatMessageRole.Assistant, firstUpdate.Role);
}
// Should contain content chunks
List<StreamingChatCompletionUpdate> contentUpdates = updates.Where(u => u.ContentUpdate.Count > 0).ToList();
Assert.NotEmpty(contentUpdates);
// Last update should have finish reason
StreamingChatCompletionUpdate? lastUpdate = updates.LastOrDefault(u => u.FinishReason != null);
Assert.NotNull(lastUpdate);
Assert.Equal(ChatFinishReason.Stop, lastUpdate.FinishReason);
}
/// <summary>
/// Verifies that streaming responses properly handle empty responses.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_EmptyResponse_HandlesGracefullyAsync()
{
// Arrange
const string AgentName = "empty-response-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
List<StreamingChatCompletionUpdate> updates = [];
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
updates.Add(update);
}
// Should still receive chunks with finish reason
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u.FinishReason == ChatFinishReason.Stop);
}
/// <summary>
/// Verifies that non-streaming responses include proper metadata.
/// </summary>
[Fact]
public async Task CreateChatCompletion_IncludesMetadata_HasRequiredFieldsAsync()
{
// Arrange
const string AgentName = "metadata-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response with metadata";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion.Id);
Assert.StartsWith("chatcmpl-", completion.Id);
Assert.NotNull(completion.Model);
Assert.NotEqual(default, completion.CreatedAt);
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
}
/// <summary>
/// Verifies that streaming responses handle very long text correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_LongText_StreamsAllContentAsync()
{
// Arrange
const string AgentName = "long-text-agent";
const string Instructions = "You are a helpful assistant.";
string expectedResponse = string.Join(" ", Enumerable.Range(1, 100).Select(i => $"Word{i}"));
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, expectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Generate long text")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
string receivedContent = contentBuilder.ToString();
Assert.Equal(expectedResponse, receivedContent);
}
/// <summary>
/// Verifies that streaming responses properly handle single-word responses.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_SingleWord_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "single-word-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
/// <summary>
/// Verifies that streaming responses preserve special characters and formatting.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_SpecialCharacters_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "special-chars-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello! How are you? I'm fine. 100% great!";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
/// <summary>
/// Verifies that non-streaming responses handle special characters correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletion_SpecialCharacters_PreservesContentAsync()
{
// Arrange
const string AgentName = "special-chars-nonstreaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Symbols: @#$%^&*() Quotes: \"Hello\" 'World' Unicode: 你好 🌍";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
string content = completion.Content[0].Text;
Assert.Equal(ExpectedResponse, content);
}
/// <summary>
/// Verifies that multiple sequential non-streaming requests work correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletion_MultipleSequentialRequests_AllSucceedAsync()
{
// Arrange
const string AgentName = "sequential-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
// Act & Assert - Make 5 sequential requests
for (int i = 0; i < 5; i++)
{
List<ChatMessage> messages =
[
new UserChatMessage($"Request {i}")
];
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
Assert.NotNull(completion);
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
Assert.Equal(ExpectedResponse, completion.Content[0].Text);
}
}
/// <summary>
/// Verifies that multiple sequential streaming requests work correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_MultipleSequentialRequests_AllStreamCorrectlyAsync()
{
// Arrange
const string AgentName = "sequential-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Streaming response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
// Act & Assert - Make 3 sequential streaming requests
for (int i = 0; i < 3; i++)
{
List<ChatMessage> messages =
[
new UserChatMessage($"Request {i}")
];
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
}
/// <summary>
/// Verifies that completion IDs are unique across multiple requests.
/// </summary>
[Fact]
public async Task CreateChatCompletion_MultipleRequests_GenerateUniqueIdsAsync()
{
// Arrange
const string AgentName = "unique-id-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
// Act
List<string> completionIds = [];
for (int i = 0; i < 10; i++)
{
List<ChatMessage> messages =
[
new UserChatMessage($"Request {i}")
];
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
completionIds.Add(completion.Id);
}
// Assert
Assert.Equal(10, completionIds.Count);
Assert.Equal(completionIds.Count, completionIds.Distinct().Count()); // All IDs should be unique
}
/// <summary>
/// Verifies that streaming responses all have the same ID within a single request.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_SameRequestId_ConsistentAcrossChunksAsync()
{
// Arrange
const string AgentName = "consistent-id-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test consistent ID across chunks";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
List<string> chunkIds = [];
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
if (!string.IsNullOrEmpty(update.CompletionId))
{
chunkIds.Add(update.CompletionId);
}
}
// All chunk IDs should be the same within a single request
Assert.NotEmpty(chunkIds);
Assert.All(chunkIds, id => Assert.Equal(chunkIds[0], id));
Assert.StartsWith("chatcmpl-", chunkIds[0]);
}
/// <summary>
/// Verifies that non-streaming responses work with system messages.
/// </summary>
[Fact]
public async Task CreateChatCompletion_WithSystemMessage_ReturnsValidResponseAsync()
{
// Arrange
const string AgentName = "system-message-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "I am following the system instructions";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new SystemChatMessage("You must respond in a specific way"),
new UserChatMessage("Hello")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion);
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
Assert.Equal(ExpectedResponse, completion.Content[0].Text);
}
/// <summary>
/// Verifies that responses handle newlines correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletion_Newlines_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "newline-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Line 1\nLine 2\nLine 3";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
string content = completion.Content[0].Text;
Assert.Equal(ExpectedResponse, content);
Assert.Contains("\n", content);
}
/// <summary>
/// Verifies that streaming responses handle newlines correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_Newlines_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "newline-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "First line\nSecond line\nThird line";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = chatClient.CompleteChatStreamingAsync(messages);
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}
}
string content = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, content);
Assert.Contains("\n", content);
}
/// <summary>
/// Verifies that responses with conversation history work correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletion_WithConversationHistory_ReturnsValidResponseAsync()
{
// Arrange
const string AgentName = "conversation-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "3 plus 3 equals 6";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("What is 2+2?"),
new AssistantChatMessage("2+2 equals 4"),
new UserChatMessage("What about 3+3?")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion);
Assert.Equal(ChatFinishReason.Stop, completion.FinishReason);
Assert.Equal(ExpectedResponse, completion.Content[0].Text);
}
/// <summary>
/// Verifies that usage information is included in non-streaming responses.
/// </summary>
[Fact]
public async Task CreateChatCompletion_IncludesUsage_HasTokenCountsAsync()
{
// Arrange
const string AgentName = "usage-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response with usage information";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ChatClient chatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Test")
];
// Act
ChatCompletion completion = await chatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion.Usage);
Assert.True(completion.Usage.InputTokenCount > 0);
Assert.True(completion.Usage.OutputTokenCount > 0);
Assert.Equal(completion.Usage.InputTokenCount + completion.Usage.OutputTokenCount, completion.Usage.TotalTokenCount);
}
/// <summary>
/// Verifies that responses with function calls work correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletion_WithFunctionCall_ReturnsToolCallsAsync()
{
// Arrange
const string AgentName = "function-call-agent";
const string Instructions = "You are a helpful assistant.";
const string FunctionName = "get_weather";
const string Arguments = "{\"location\":\"Seattle\"}";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
ChatClient openAIChatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("What's the weather?")
];
// Act
ChatCompletion completion = await openAIChatClient.CompleteChatAsync(messages);
// Assert
Assert.NotNull(completion);
Assert.Equal(ChatFinishReason.ToolCalls, completion.FinishReason);
Assert.NotNull(completion.ToolCalls);
Assert.NotEmpty(completion.ToolCalls);
ChatToolCall toolCall = completion.ToolCalls[0];
Assert.Equal(FunctionName, toolCall.FunctionName);
Assert.NotNull(toolCall.FunctionArguments);
}
/// <summary>
/// Verifies that streaming responses with function calls work correctly.
/// </summary>
[Fact]
public async Task CreateChatCompletionStreaming_WithFunctionCall_StreamsToolCallsAsync()
{
// Arrange
const string AgentName = "function-call-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string FunctionName = "calculate";
const string Arguments = "{\"expression\":\"2+2\"}";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
ChatClient openAIChatClient = this.CreateChatClient(AgentName);
List<ChatMessage> messages =
[
new UserChatMessage("Calculate 2+2")
];
// Act
AsyncCollectionResult<StreamingChatCompletionUpdate> streamingResult = openAIChatClient.CompleteChatStreamingAsync(messages);
// Assert
List<StreamingChatCompletionUpdate> updates = [];
await foreach (StreamingChatCompletionUpdate update in streamingResult)
{
updates.Add(update);
}
Assert.NotEmpty(updates);
// Should have finish reason of tool_calls
StreamingChatCompletionUpdate? lastUpdate = updates.LastOrDefault(u => u.FinishReason != null);
Assert.NotNull(lastUpdate);
Assert.True(lastUpdate.FinishReason is ChatFinishReason.ToolCalls or ChatFinishReason.Stop); // depends on what response we get
}
private ChatClient CreateChatClient(string agentName)
{
return new ChatClient(
model: "test-model",
credential: new ApiKeyCredential("test-api-key"),
options: new OpenAIClientOptions
{
Endpoint = new Uri(this._httpClient!.BaseAddress!, $"/{agentName}/v1/"),
Transport = new HttpClientPipelineTransport(this._httpClient)
});
}
private async Task<HttpClient> CreateTestServerAsync(string agentName, string instructions, string responseText = "Test response")
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddOpenAIChatCompletions();
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task<HttpClient> CreateTestServerWithCustomClientAsync(string agentName, string instructions, IChatClient chatClient)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}");
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task<HttpClient> CreateTestServerWithMultipleAgentsAsync(
params (string Name, string Instructions, string ResponseText)[] agents)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
foreach ((string name, string instructions, string responseText) in agents)
{
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton($"chat-client-{name}", mockChatClient);
builder.AddAIAgent(name, instructions, chatClientServiceKey: $"chat-client-{name}");
}
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
foreach ((string name, string _, string _) in agents)
{
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(name);
this._app.MapOpenAIChatCompletions(agent);
}
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
}
@@ -0,0 +1,576 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Tests;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Tests for OpenAI ChatCompletions API model serialization and deserialization.
/// These tests verify that our models correctly serialize to and deserialize from JSON
/// matching the OpenAI wire format, without testing actual API implementation behavior.
/// </summary>
public sealed class OpenAIChatCompletionsSerializationTests : ConformanceTestBase
{
#region Request Deserialization Tests
[Fact]
public void Deserialize_BasicRequest_Success()
{
// Arrange
string json = LoadChatCompletionsTraceFile("basic/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.Equal("gpt-4o-mini", request.Model);
Assert.NotNull(request.Messages);
Assert.True(request.Messages.Count > 0);
Assert.Equal(100, request.MaxCompletionTokens);
}
[Fact]
public void Deserialize_BasicRequest_RoundTrip()
{
// Arrange
string originalJson = LoadChatCompletionsTraceFile("basic/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(originalJson, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
string reserializedJson = JsonSerializer.Serialize(request, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
CreateChatCompletion? roundtripped = JsonSerializer.Deserialize(reserializedJson, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(roundtripped);
Assert.Equal(request.Model, roundtripped.Model);
Assert.Equal(request.MaxCompletionTokens, roundtripped.MaxCompletionTokens);
Assert.Equal(request.Messages.Count, roundtripped.Messages.Count);
}
[Fact]
public void Deserialize_BasicRequest_HasMessages()
{
// Arrange
string json = LoadChatCompletionsTraceFile("basic/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Messages);
Assert.Single(request.Messages);
var message = request.Messages[0];
Assert.Equal("user", message.Role);
Assert.NotNull(message.Content);
}
[Fact]
public void Deserialize_StreamingRequest_HasStreamFlag()
{
// Arrange
string json = LoadChatCompletionsTraceFile("streaming/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.True(request.Stream);
Assert.Equal(150, request.MaxCompletionTokens);
}
[Fact]
public void Deserialize_SystemMessageRequest_HasSystemRole()
{
// Arrange
string json = LoadChatCompletionsTraceFile("system_message/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Messages);
Assert.True(request.Messages.Count >= 2);
Assert.Equal("system", request.Messages[0].Role);
Assert.Equal("user", request.Messages[1].Role);
}
[Fact]
public void Deserialize_MultiTurnRequest_HasMultipleMessages()
{
// Arrange
string json = LoadChatCompletionsTraceFile("multi_turn/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Messages);
Assert.True(request.Messages.Count >= 3);
Assert.Equal("user", request.Messages[0].Role);
Assert.Equal("assistant", request.Messages[1].Role);
Assert.Equal("user", request.Messages[2].Role);
}
[Fact]
public void Deserialize_FunctionCallingRequest_HasTools()
{
// Arrange
string json = LoadChatCompletionsTraceFile("function_calling/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Tools);
Assert.Single(request.Tools);
Assert.NotNull(request.ToolChoice?.Mode);
Assert.Equal("auto", request.ToolChoice.Mode);
}
[Fact]
public void Deserialize_JsonModeRequest_HasResponseFormat()
{
// Arrange
string json = LoadChatCompletionsTraceFile("json_mode/request.json");
// Act
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.ResponseFormat);
}
[Fact]
public void Deserialize_AllRequests_CanBeDeserialized()
{
// Arrange
string[] requestPaths =
[
"basic/request.json",
"streaming/request.json",
"system_message/request.json",
"multi_turn/request.json",
"function_calling/request.json",
"json_mode/request.json"
];
foreach (var path in requestPaths)
{
string json = LoadChatCompletionsTraceFile(path);
// Act & Assert - Should not throw
CreateChatCompletion? request = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.CreateChatCompletion);
Assert.NotNull(request);
Assert.NotNull(request.Messages);
Assert.True(request.Messages.Count > 0, $"Request from {path} should have messages");
}
}
#endregion
#region Response Deserialization Tests
[Fact]
public void Deserialize_BasicResponse_Success()
{
// Arrange
string json = LoadChatCompletionsTraceFile("basic/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.StartsWith("chatcmpl-", response.Id);
Assert.Equal("chat.completion", response.Object);
Assert.True(response.Created > 0);
Assert.NotNull(response.Model);
Assert.StartsWith("gpt-4o-mini", response.Model);
}
[Fact]
public void Deserialize_BasicResponse_HasChoices()
{
// Arrange
string json = LoadChatCompletionsTraceFile("basic/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Choices);
Assert.Single(response.Choices);
var choice = response.Choices[0];
Assert.Equal(0, choice.Index);
Assert.NotNull(choice.Message);
Assert.Equal("assistant", choice.Message.Role);
Assert.NotNull(choice.Message.Content);
Assert.NotNull(choice.FinishReason);
}
[Fact]
public void Deserialize_BasicResponse_HasUsage()
{
// Arrange
string json = LoadChatCompletionsTraceFile("basic/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Usage);
Assert.True(response.Usage.PromptTokens > 0);
Assert.True(response.Usage.CompletionTokens > 0);
Assert.Equal(response.Usage.PromptTokens + response.Usage.CompletionTokens, response.Usage.TotalTokens);
Assert.NotNull(response.Usage.PromptTokensDetails);
Assert.NotNull(response.Usage.CompletionTokensDetails);
}
[Fact]
public void Deserialize_SystemMessageResponse_HasContent()
{
// Arrange
string json = LoadChatCompletionsTraceFile("system_message/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Choices);
var message = response.Choices[0].Message;
Assert.Equal("assistant", message.Role);
Assert.NotNull(message.Content);
Assert.Contains("Ahoy, matey", message.Content, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Deserialize_MultiTurnResponse_HasContent()
{
// Arrange
string json = LoadChatCompletionsTraceFile("multi_turn/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Choices);
var message = response.Choices[0].Message;
Assert.Equal("assistant", message.Role);
Assert.NotNull(message.Content);
}
[Fact]
public void Deserialize_FunctionCallingResponse_HasToolCalls()
{
// Arrange
string json = LoadChatCompletionsTraceFile("function_calling/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Choices);
var choice = response.Choices[0];
Assert.Equal("tool_calls", choice.FinishReason);
var message = choice.Message;
Assert.NotNull(message.ToolCalls);
Assert.Single(message.ToolCalls);
var toolCall = message.ToolCalls[0];
Assert.NotNull(toolCall.Id);
Assert.StartsWith("call_", toolCall.Id);
Assert.Equal("function", toolCall.Type);
Assert.NotNull(toolCall.Function);
Assert.Equal("get_weather", toolCall.Function.Name);
Assert.NotNull(toolCall.Function.Arguments);
}
[Fact]
public void Deserialize_JsonModeResponse_HasStructuredOutput()
{
// Arrange
string json = LoadChatCompletionsTraceFile("json_mode/response.json");
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Choices);
var message = response.Choices[0].Message;
Assert.NotNull(message.Content);
// Verify the content is valid JSON
using var jsonDoc = JsonDocument.Parse(message.Content);
var jsonRoot = jsonDoc.RootElement;
Assert.Equal(JsonValueKind.Object, jsonRoot.ValueKind);
Assert.True(jsonRoot.TryGetProperty("name", out _));
Assert.True(jsonRoot.TryGetProperty("age", out _));
Assert.True(jsonRoot.TryGetProperty("occupation", out _));
}
[Fact]
public void Deserialize_AllResponses_HaveRequiredFields()
{
// Arrange
string[] responsePaths =
[
"basic/response.json",
"system_message/response.json",
"multi_turn/response.json",
"function_calling/response.json",
"json_mode/response.json"
];
foreach (var path in responsePaths)
{
string json = LoadChatCompletionsTraceFile(path);
// Act
ChatCompletion? response = JsonSerializer.Deserialize(json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Id);
Assert.Equal("chat.completion", response.Object);
Assert.True(response.Created > 0, $"Response from {path} should have created timestamp");
Assert.NotNull(response.Model);
Assert.NotNull(response.Choices);
Assert.True(response.Choices.Count > 0, $"Response from {path} should have choices");
}
}
[Fact]
public void Deserialize_ResponseRoundTrip_PreservesData()
{
// Arrange
string originalJson = LoadChatCompletionsTraceFile("basic/response.json");
// Act - Deserialize and re-serialize
ChatCompletion? response = JsonSerializer.Deserialize(originalJson, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
string reserializedJson = JsonSerializer.Serialize(response, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
ChatCompletion? roundtripped = JsonSerializer.Deserialize(reserializedJson, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletion);
// Assert
Assert.NotNull(response);
Assert.NotNull(roundtripped);
Assert.Equal(response.Id, roundtripped.Id);
Assert.Equal(response.Created, roundtripped.Created);
Assert.Equal(response.Model, roundtripped.Model);
Assert.Equal(response.Choices.Count, roundtripped.Choices.Count);
}
#endregion
#region Streaming Chunk Deserialization Tests
[Fact]
public void ParseStreamingChunks_BasicFormat_Success()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
// Assert
Assert.NotEmpty(chunks);
Assert.All(chunks, chunk =>
{
ChatCompletionChunk? parsed = JsonSerializer.Deserialize(chunk.GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk);
Assert.NotNull(parsed);
Assert.NotNull(parsed.Id);
Assert.Equal("chat.completion.chunk", parsed.Object);
Assert.True(parsed.Created > 0);
Assert.NotNull(parsed.Model);
Assert.NotNull(parsed.Choices);
});
}
[Fact]
public void ParseStreamingChunks_AllChunksSameId()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
// Deserialize chunks
var parsedChunks = chunks
.Select(c => JsonSerializer.Deserialize(c.GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk))
.Where(c => c != null)
.ToList();
// Assert
Assert.NotEmpty(parsedChunks);
string? firstId = parsedChunks[0]!.Id;
Assert.NotNull(firstId);
Assert.StartsWith("chatcmpl-", firstId);
Assert.All(parsedChunks, chunk => Assert.Equal(firstId, chunk!.Id));
}
[Fact]
public void ParseStreamingChunks_FirstChunkHasRole()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
var firstChunk = JsonSerializer.Deserialize(chunks[0].GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk);
// Assert
Assert.NotNull(firstChunk);
Assert.NotNull(firstChunk.Choices);
Assert.True(firstChunk.Choices.Count > 0);
var firstChoice = firstChunk.Choices[0];
Assert.NotNull(firstChoice.Delta);
if (firstChoice.Delta.Role != null)
{
Assert.Equal("assistant", firstChoice.Delta.Role);
}
}
[Fact]
public void ParseStreamingChunks_AccumulateContent_MatchesExpected()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
var contentPieces = new List<string>();
foreach (var chunkJson in chunks)
{
var chunk = JsonSerializer.Deserialize(chunkJson.GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk);
if (chunk?.Choices != null && chunk.Choices.Count > 0)
{
var delta = chunk.Choices[0].Delta;
if (!string.IsNullOrEmpty(delta?.Content))
{
contentPieces.Add(delta.Content);
}
}
}
// Assert
Assert.NotEmpty(contentPieces);
string fullText = string.Concat(contentPieces);
Assert.NotEmpty(fullText);
Assert.Contains("circuits", fullText);
Assert.Contains("flight", fullText);
}
[Fact]
public void ParseStreamingChunks_LastChunkHasFinishReason()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
// Find chunks with finish_reason
var chunksWithFinishReason = new List<ChatCompletionChunk>();
foreach (var chunkJson in chunks)
{
var chunk = JsonSerializer.Deserialize(chunkJson.GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk);
if (chunk?.Choices != null && chunk.Choices.Count > 0 && !string.IsNullOrEmpty(chunk.Choices[0].FinishReason))
{
chunksWithFinishReason.Add(chunk);
}
}
// Assert
Assert.NotEmpty(chunksWithFinishReason);
var lastChunk = chunksWithFinishReason.Last();
Assert.Contains(lastChunk.Choices[0].FinishReason, collection: ["stop", "length", "tool_calls", "content_filter"]);
}
[Fact]
public void ParseStreamingChunks_LastChunkHasUsage()
{
// Arrange
string sseContent = LoadChatCompletionsTraceFile("streaming/response.txt");
// Act
var chunks = ParseChatCompletionChunksFromSse(sseContent);
var lastChunkJson = chunks.Last();
var lastChunk = JsonSerializer.Deserialize(lastChunkJson.GetRawText(), ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionChunk);
// Assert
Assert.NotNull(lastChunk);
Assert.NotNull(lastChunk.Usage);
Assert.True(lastChunk.Usage.PromptTokens > 0);
Assert.True(lastChunk.Usage.CompletionTokens > 0);
Assert.Equal(lastChunk.Usage.PromptTokens + lastChunk.Usage.CompletionTokens, lastChunk.Usage.TotalTokens);
}
/// <summary>
/// Helper to parse chat completion chunks from SSE response.
/// </summary>
private static List<JsonElement> ParseChatCompletionChunksFromSse(string sseContent)
{
var chunks = new List<JsonElement>();
var lines = sseContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].TrimEnd('\r');
if (line.StartsWith("data: ", StringComparison.Ordinal))
{
var jsonData = line.Substring("data: ".Length);
// Skip [DONE] marker
if (jsonData == "[DONE]")
{
continue;
}
try
{
var doc = JsonDocument.Parse(jsonData);
chunks.Add(doc.RootElement.Clone());
}
catch
{
// Skip invalid JSON
}
}
}
return chunks;
}
#endregion
}
@@ -22,8 +22,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task BasicRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("basic/request.json");
using var expectedResponseDoc = LoadTraceDocument("basic/response.json");
string requestJson = LoadResponsesTraceFile("basic/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("basic/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get the expected response text from the trace to use as mock response
@@ -34,7 +34,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("basic-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "basic-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "basic-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -160,8 +160,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ConversationRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("conversation/request.json");
using var expectedResponseDoc = LoadTraceDocument("conversation/response.json");
string requestJson = LoadResponsesTraceFile("conversation/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("conversation/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get the expected response text
@@ -172,7 +172,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("conversation-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "conversation-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "conversation-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -270,8 +270,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ToolCallRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("tool_call/request.json");
using var expectedResponseDoc = LoadTraceDocument("tool_call/response.json");
string requestJson = LoadResponsesTraceFile("tool_call/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("tool_call/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get function call details from expected response
@@ -282,7 +282,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("tool-agent", "You are a helpful assistant.", functionName);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "tool-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "tool-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -429,8 +429,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task StreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedResponseSse = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedResponseSse = LoadResponsesTraceFile("streaming/response.txt");
// Extract expected text from SSE events
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
@@ -440,7 +440,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
@@ -634,8 +634,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task MetadataRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("metadata/request.json");
using var expectedResponseDoc = LoadTraceDocument("metadata/response.json");
string requestJson = LoadResponsesTraceFile("metadata/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("metadata/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected text (truncated due to max_output_tokens)
@@ -646,7 +646,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("metadata-agent", "Respond in a friendly, educational tone.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "metadata-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "metadata-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -761,8 +761,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ReasoningRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("reasoning/request.json");
using var expectedResponseDoc = LoadTraceDocument("reasoning/response.json");
string requestJson = LoadResponsesTraceFile("reasoning/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("reasoning/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected text from the message output
@@ -773,7 +773,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("reasoning-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "reasoning-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "reasoning-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -843,8 +843,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task JsonOutputRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("json_output/request.json");
using var expectedResponseDoc = LoadTraceDocument("json_output/response.json");
string requestJson = LoadResponsesTraceFile("json_output/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("json_output/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected JSON text from response
@@ -855,7 +855,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("json-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "json-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "json-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -927,8 +927,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task RefusalRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("refusal/request.json");
using var expectedResponseDoc = LoadTraceDocument("refusal/response.json");
string requestJson = LoadResponsesTraceFile("refusal/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("refusal/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected refusal text
@@ -939,7 +939,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("refusal-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "refusal-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "refusal-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -986,8 +986,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ImageInputRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("image_input/request.json");
using var expectedResponseDoc = LoadTraceDocument("image_input/response.json");
string requestJson = LoadResponsesTraceFile("image_input/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("image_input/response.json");
var expectedResponse = expectedResponseDoc.RootElement;
// Get expected text
@@ -998,7 +998,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("image-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "image-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "image-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
@@ -1059,8 +1059,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ReasoningStreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("reasoning_streaming/request.json");
string expectedResponseSse = LoadTraceFile("reasoning_streaming/response.txt");
string requestJson = LoadResponsesTraceFile("reasoning_streaming/request.json");
string expectedResponseSse = LoadResponsesTraceFile("reasoning_streaming/response.txt");
// Extract expected text from SSE events
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
@@ -1070,7 +1070,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("reasoning-streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "reasoning-streaming-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "reasoning-streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
@@ -1137,8 +1137,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task JsonOutputStreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("json_output_streaming/request.json");
string expectedResponseSse = LoadTraceFile("json_output_streaming/response.txt");
string requestJson = LoadResponsesTraceFile("json_output_streaming/request.json");
string expectedResponseSse = LoadResponsesTraceFile("json_output_streaming/response.txt");
// Extract expected text from SSE events
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
@@ -1148,7 +1148,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("json-streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "json-streaming-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "json-streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
@@ -1197,8 +1197,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task RefusalStreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("refusal_streaming/request.json");
string expectedResponseSse = LoadTraceFile("refusal_streaming/response.txt");
string requestJson = LoadResponsesTraceFile("refusal_streaming/request.json");
string expectedResponseSse = LoadResponsesTraceFile("refusal_streaming/response.txt");
// Extract expected text from SSE events
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
@@ -1208,7 +1208,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("refusal-streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "refusal-streaming-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "refusal-streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
@@ -1254,8 +1254,8 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
public async Task ImageInputStreamingRequestResponseAsync()
{
// Arrange
string requestJson = LoadTraceFile("image_input_streaming/request.json");
string expectedResponseSse = LoadTraceFile("image_input_streaming/response.txt");
string requestJson = LoadResponsesTraceFile("image_input_streaming/request.json");
string expectedResponseSse = LoadResponsesTraceFile("image_input_streaming/response.txt");
// Extract expected text from SSE events
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
@@ -1265,7 +1265,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("image-streaming-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "image-streaming-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "image-streaming-agent", requestJson);
// Assert - Response should be SSE format
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
@@ -22,7 +22,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_BasicRequest_Success()
{
// Arrange
string json = LoadTraceFile("basic/request.json");
string json = LoadResponsesTraceFile("basic/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -38,7 +38,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_BasicRequest_RoundTrip()
{
// Arrange
string originalJson = LoadTraceFile("basic/request.json");
string originalJson = LoadResponsesTraceFile("basic/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -56,7 +56,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_StreamingRequest_HasStreamFlag()
{
// Arrange
string json = LoadTraceFile("streaming/request.json");
string json = LoadResponsesTraceFile("streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -71,7 +71,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ConversationRequest_HasPreviousResponseId()
{
// Arrange
string json = LoadTraceFile("conversation/request.json");
string json = LoadResponsesTraceFile("conversation/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -86,7 +86,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_MetadataRequest_HasAllParameters()
{
// Arrange
string json = LoadTraceFile("metadata/request.json");
string json = LoadResponsesTraceFile("metadata/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -111,7 +111,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ToolCallRequest_HasToolDefinitions()
{
// Arrange
string json = LoadTraceFile("tool_call/request.json");
string json = LoadResponsesTraceFile("tool_call/request.json");
// Act
// CreateResponse doesn't have Tools property - it uses dynamic JSON
@@ -220,7 +220,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ImageInputRequest_HasImageData()
{
// Arrange
string json = LoadTraceFile("image_input/request.json");
string json = LoadResponsesTraceFile("image_input/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -234,7 +234,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ImageInputStreamingRequest_HasStreamAndImage()
{
// Arrange
string json = LoadTraceFile("image_input_streaming/request.json");
string json = LoadResponsesTraceFile("image_input_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -249,7 +249,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_JsonOutputRequest_HasJsonSchema()
{
// Arrange
string json = LoadTraceFile("json_output/request.json");
string json = LoadResponsesTraceFile("json_output/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -270,7 +270,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_JsonOutputStreamingRequest_HasJsonSchemaAndStream()
{
// Arrange
string json = LoadTraceFile("json_output_streaming/request.json");
string json = LoadResponsesTraceFile("json_output_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -290,7 +290,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ReasoningRequest_HasReasoningConfiguration()
{
// Arrange
string json = LoadTraceFile("reasoning/request.json");
string json = LoadResponsesTraceFile("reasoning/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -304,7 +304,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ReasoningStreamingRequest_HasReasoningAndStream()
{
// Arrange
string json = LoadTraceFile("reasoning_streaming/request.json");
string json = LoadResponsesTraceFile("reasoning_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -319,7 +319,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_RefusalRequest_CanBeDeserialized()
{
// Arrange
string json = LoadTraceFile("refusal/request.json");
string json = LoadResponsesTraceFile("refusal/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -333,7 +333,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_RefusalStreamingRequest_HasStream()
{
// Arrange
string json = LoadTraceFile("refusal_streaming/request.json");
string json = LoadResponsesTraceFile("refusal_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -367,7 +367,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var path in requestPaths)
{
string json = LoadTraceFile(path);
string json = LoadResponsesTraceFile(path);
// Act & Assert - Should not throw
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
@@ -384,7 +384,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_BasicResponse_Success()
{
// Arrange
string json = LoadTraceFile("basic/response.json");
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -403,7 +403,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_BasicResponse_HasCorrectOutput()
{
// Arrange
string json = LoadTraceFile("basic/response.json");
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -426,7 +426,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_BasicResponse_HasCorrectUsage()
{
// Arrange
string json = LoadTraceFile("basic/response.json");
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -445,7 +445,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ConversationResponse_HasPreviousResponseId()
{
// Arrange
string json = LoadTraceFile("conversation/response.json");
string json = LoadResponsesTraceFile("conversation/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -461,7 +461,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_MetadataResponse_PreservesMetadata()
{
// Arrange
string json = LoadTraceFile("metadata/response.json");
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -478,7 +478,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_MetadataResponse_HasIncompleteStatus()
{
// Arrange
string json = LoadTraceFile("metadata/response.json");
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -494,7 +494,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_MetadataResponse_HasInstructions()
{
// Arrange
string json = LoadTraceFile("metadata/response.json");
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -509,7 +509,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_MetadataResponse_HasModelParameters()
{
// Arrange
string json = LoadTraceFile("metadata/response.json");
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -525,7 +525,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ToolCallResponse_HasFunctionCall()
{
// Arrange
string json = LoadTraceFile("tool_call/response.json");
string json = LoadResponsesTraceFile("tool_call/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -549,7 +549,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ToolCallResponse_HasToolDefinitions()
{
// Arrange
string json = LoadTraceFile("tool_call/response.json");
string json = LoadResponsesTraceFile("tool_call/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -573,7 +573,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ImageInputResponse_HasImageInInput()
{
// Arrange
string json = LoadTraceFile("image_input/response.json");
string json = LoadResponsesTraceFile("image_input/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -588,7 +588,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_JsonOutputResponse_HasStructuredOutput()
{
// Arrange
string json = LoadTraceFile("json_output/response.json");
string json = LoadResponsesTraceFile("json_output/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -608,7 +608,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ReasoningResponse_HasReasoningItems()
{
// Arrange
string json = LoadTraceFile("reasoning/response.json");
string json = LoadResponsesTraceFile("reasoning/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -624,7 +624,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_RefusalResponse_HasRefusalContent()
{
// Arrange
string json = LoadTraceFile("refusal/response.json");
string json = LoadResponsesTraceFile("refusal/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -653,7 +653,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var path in responsePaths)
{
string json = LoadTraceFile(path);
string json = LoadResponsesTraceFile(path);
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
@@ -672,7 +672,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void Deserialize_ResponseRoundTrip_PreservesData()
{
// Arrange
string originalJson = LoadTraceFile("basic/response.json");
string originalJson = LoadResponsesTraceFile("basic/response.json");
// Act - Deserialize and re-serialize
Response? response = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.Response);
@@ -696,7 +696,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_BasicFormat_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -715,7 +715,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_HasCorrectEventTypes()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -736,7 +736,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_DeserializeCreatedEvent_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var createdEventJson = events.First(e => e.GetProperty("type").GetString() == "response.created");
@@ -758,7 +758,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_DeserializeInProgressEvent_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var inProgressEventJson = events.First(e => e.GetProperty("type").GetString() == "response.in_progress");
@@ -779,7 +779,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_DeserializeOutputItemAdded_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var itemAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.output_item.added");
@@ -799,7 +799,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_DeserializeContentPartAdded_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var partAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.content_part.added");
@@ -821,7 +821,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_DeserializeTextDelta_Success()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var textDeltaJson = events.First(e => e.GetProperty("type").GetString() == "response.output_text.delta");
@@ -843,7 +843,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalText()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
// Act
@@ -877,7 +877,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_SequenceNumbersAreSequential()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
// Act
@@ -904,7 +904,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_FinalEvent_IsTerminalState()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
var events = ParseSseEventsFromContent(sseContent);
var lastEventJson = events.Last();
@@ -926,7 +926,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_ImageInputStreaming_HasImageEvents()
{
// Arrange
string sseContent = LoadTraceFile("image_input_streaming/response.txt");
string sseContent = LoadResponsesTraceFile("image_input_streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -944,7 +944,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_JsonOutputStreaming_HasJsonSchemaEvents()
{
// Arrange
string sseContent = LoadTraceFile("json_output_streaming/response.txt");
string sseContent = LoadResponsesTraceFile("json_output_streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -962,7 +962,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_ReasoningStreaming_HasReasoningEvents()
{
// Arrange
string sseContent = LoadTraceFile("reasoning_streaming/response.txt");
string sseContent = LoadResponsesTraceFile("reasoning_streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -983,7 +983,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_RefusalStreaming_HasRefusalEvents()
{
// Arrange
string sseContent = LoadTraceFile("refusal_streaming/response.txt");
string sseContent = LoadResponsesTraceFile("refusal_streaming/response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
@@ -1014,7 +1014,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var path in streamingPaths)
{
string sseContent = LoadTraceFile(path);
string sseContent = LoadResponsesTraceFile(path);
// Act & Assert
foreach (var eventJson in ParseSseEventsFromContent(sseContent))
@@ -1030,7 +1030,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
public void ParseStreamingEvents_AllEvents_CanBeDeserialized()
{
// Arrange
string sseContent = LoadTraceFile("streaming/response.txt");
string sseContent = LoadResponsesTraceFile("streaming/response.txt");
// Act & Assert
foreach (var eventJson in ParseSseEventsFromContent(sseContent))
@@ -24,8 +24,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_BasicFormat_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
// Extract expected text
var expectedEvents = ParseSseEvents(expectedSseContent);
@@ -35,7 +35,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-basic-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-basic-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-basic-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
// Act
@@ -55,8 +55,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_HasCorrectEventTypesAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -65,7 +65,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-types-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-types-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-types-agent", requestJson);
// Assert - HTTP response validation
Assert.Equal(System.Net.HttpStatusCode.OK, httpResponse.StatusCode);
@@ -118,8 +118,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_DeserializeCreatedEvent_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -128,7 +128,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-created-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-created-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-created-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var createdEventJson = events.First(e => e.GetProperty("type").GetString() == "response.created");
@@ -151,8 +151,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_DeserializeInProgressEvent_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -161,7 +161,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-progress-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-progress-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-progress-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var inProgressEventJson = events.First(e => e.GetProperty("type").GetString() == "response.in_progress");
@@ -183,8 +183,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_DeserializeOutputItemAdded_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -193,7 +193,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-item-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-item-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-item-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var itemAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.output_item.added");
@@ -214,8 +214,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_DeserializeContentPartAdded_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -224,7 +224,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-part-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-part-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-part-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var partAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.content_part.added");
@@ -247,8 +247,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_DeserializeTextDelta_SuccessAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -257,7 +257,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-delta-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-delta-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-delta-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var textDeltaJson = events.First(e => e.GetProperty("type").GetString() == "response.output_text.delta");
@@ -280,8 +280,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalTextAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -290,7 +290,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-accumulate-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-accumulate-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-accumulate-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -325,8 +325,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_SequenceNumbersAreSequentialAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -335,7 +335,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-sequence-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-sequence-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-sequence-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -363,8 +363,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_FinalEvent_IsTerminalStateAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -373,7 +373,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-terminal-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-terminal-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-terminal-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
var lastEventJson = events.Last();
@@ -396,8 +396,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_AllEvents_CanBeDeserializedAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -406,7 +406,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-deserialize-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-deserialize-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-deserialize-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
// Act & Assert
@@ -439,8 +439,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_IdConsistency_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -449,7 +449,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-id-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-id-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-id-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -530,8 +530,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_IndexConsistency_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -540,7 +540,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-index-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-index-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-index-agent", requestJson);
// Assert - All events with output_index should have valid values
foreach (var eventJson in ParseSseEvents(await httpResponse.Content.ReadAsStringAsync()))
@@ -587,8 +587,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_ResponseObjectEvolution_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -597,7 +597,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-evolution-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-evolution-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-evolution-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -655,8 +655,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_SseFormatCompliance_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -665,7 +665,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-sse-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-sse-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-sse-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
// Assert - SSE format validation
@@ -699,8 +699,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_EventPairing_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -709,7 +709,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-pairing-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-pairing-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-pairing-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);
@@ -755,8 +755,8 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
public async Task ParseStreamingEvents_NoDuplicateSequenceNumbers_ValidAsync()
{
// Arrange
string requestJson = LoadTraceFile("streaming/request.json");
string expectedSseContent = LoadTraceFile("streaming/response.txt");
string requestJson = LoadResponsesTraceFile("streaming/request.json");
string expectedSseContent = LoadResponsesTraceFile("streaming/response.txt");
var expectedEvents = ParseSseEvents(expectedSseContent);
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
@@ -765,7 +765,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
HttpClient client = await this.CreateTestServerAsync("streaming-nodup-agent", "You are a helpful assistant.", expectedText);
// Act
HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "streaming-nodup-agent", requestJson);
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "streaming-nodup-agent", requestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEvents(sseContent);