mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
@@ -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>
|
||||
|
||||
+12
@@ -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
|
||||
}
|
||||
+33
@@ -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"
|
||||
}
|
||||
+34
@@ -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"
|
||||
}
|
||||
+43
@@ -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"
|
||||
}
|
||||
+36
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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"
|
||||
}
|
||||
+18
@@ -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
|
||||
}
|
||||
+33
@@ -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"
|
||||
}
|
||||
+12
@@ -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
|
||||
}
|
||||
+21
@@ -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]
|
||||
+14
@@ -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
|
||||
}
|
||||
+33
@@ -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"
|
||||
}
|
||||
+19
-19
@@ -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);
|
||||
|
||||
|
||||
+13
-1
@@ -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>
|
||||
|
||||
+495
@@ -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;
|
||||
}
|
||||
}
|
||||
+974
@@ -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();
|
||||
}
|
||||
}
|
||||
+576
@@ -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
|
||||
}
|
||||
+39
-39
@@ -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);
|
||||
|
||||
+47
-47
@@ -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))
|
||||
|
||||
+51
-51
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user