.NET: Improve fidelity of OpenAI Responses server and add Conversations (#1907)

* Improve fidelity of OpenAI Responses server and add Conversations

* Merge

* nit

* Undo prior change

* Undo prior change

* Review feedback

* Review feedback

* Fix test

* Use simpler JsonDocument approach for polymorphic deserialization

* More review feedback

* dotnet format
This commit is contained in:
Reuben Bond
2025-11-05 18:46:19 +00:00
committed by GitHub
parent e2282ebe42
commit 33f84f9ed2
135 changed files with 11718 additions and 1243 deletions
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for AgentInvocationContext.
/// </summary>
public sealed class AgentInvocationContextTests
{
[Fact]
public void Constructor_WithIdGenerator_InitializesCorrectly()
{
// Arrange
var idGenerator = new IdGenerator("resp_test123", "conv_test456");
// Act
var context = new AgentInvocationContext(idGenerator);
// Assert
Assert.NotNull(context);
Assert.Same(idGenerator, context.IdGenerator);
Assert.Equal("resp_test123", context.ResponseId);
Assert.Equal("conv_test456", context.ConversationId);
Assert.NotNull(context.JsonSerializerOptions);
}
[Fact]
public void Constructor_WithoutJsonOptions_UsesDefaultOptions()
{
// Arrange
var idGenerator = new IdGenerator("resp_test", "conv_test");
// Act
var context = new AgentInvocationContext(idGenerator);
// Assert
Assert.NotNull(context.JsonSerializerOptions);
Assert.Same(OpenAIHostingJsonUtilities.DefaultOptions, context.JsonSerializerOptions);
}
[Fact]
public void Constructor_WithCustomJsonOptions_UsesProvidedOptions()
{
// Arrange
var idGenerator = new IdGenerator("resp_test", "conv_test");
var customOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Act
var context = new AgentInvocationContext(idGenerator, customOptions);
// Assert
Assert.Same(customOptions, context.JsonSerializerOptions);
}
[Fact]
public void ResponseId_ReturnsIdGeneratorResponseId()
{
// Arrange
const string ResponseId = "resp_property_test";
var idGenerator = new IdGenerator(ResponseId, "conv_test");
var context = new AgentInvocationContext(idGenerator);
// Act
string result = context.ResponseId;
// Assert
Assert.Equal(ResponseId, result);
Assert.Equal(idGenerator.ResponseId, result);
}
[Fact]
public void ConversationId_ReturnsIdGeneratorConversationId()
{
// Arrange
const string ConversationId = "conv_property_test";
var idGenerator = new IdGenerator("resp_test", ConversationId);
var context = new AgentInvocationContext(idGenerator);
// Act
string result = context.ConversationId;
// Assert
Assert.Equal(ConversationId, result);
Assert.Equal(idGenerator.ConversationId, result);
}
}
@@ -226,6 +226,37 @@ public abstract class ConformanceTestBase : IAsyncDisposable
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
this._httpClient = testServer.CreateClient();
return this._httpClient;
}
/// <summary>
/// Creates a test server with a mock chat client that returns function call content.
/// </summary>
protected async Task<HttpClient> CreateTestServerWithToolCallAsync(
string agentName,
string instructions,
string functionName,
string arguments)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.ToolCallMockChatClient(functionName, arguments);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
@@ -0,0 +1,24 @@
{
"items": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is the weather like today?"
}
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Tell me a joke!"
}
]
}
]
}
@@ -0,0 +1,32 @@
{
"object": "list",
"data": [
{
"id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "What is the weather like today?"
}
],
"role": "user"
},
{
"id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "Tell me a joke!"
}
],
"role": "user"
}
],
"first_id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"has_more": false,
"last_id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822"
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "basic_conversation"
}
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"object": "conversation",
"created_at": 1761318654,
"metadata": {
"test_type": "basic_conversation"
}
}
@@ -0,0 +1,6 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"input": "What is the capital of France?",
"max_output_tokens": 100
}
@@ -0,0 +1,70 @@
{
"id": "resp_04cbf451511948220068fb97bdec548195a367870aa85734de",
"object": "response",
"created_at": 1761318846,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 100,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_04cbf451511948220068fb97c0162881958d80862a0d253a14",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The capital of France is Paris."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 36,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 8,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 44
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,6 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"input": "What is its population?",
"max_output_tokens": 150
}
@@ -0,0 +1,70 @@
{
"id": "resp_04cbf451511948220068fb97cf320881958b69530fe07eb2a9",
"object": "response",
"created_at": 1761318863,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 150,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the larger metropolitan area has a population of around 12 million. These numbers can vary, so it's always a good idea to check for the most recent statistics."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 56,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 58,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 114
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,624 @@
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"in_progress","content":[],"role":"assistant"}}
event: response.content_part.added
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"In","logprobs":[],"obfuscation":"C16oYk8aI5VtGp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"vXmOvISW7QRUF1"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"qEkC6mYZmi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" workshop","logprobs":[],"obfuscation":"2aAdNXN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" at","logprobs":[],"obfuscation":"bv66grEvpSema"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fVOKa91q3jxh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" edge","logprobs":[],"obfuscation":"kW1rIr6ZZBc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"RnPLx5DWhJvWO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"DMVs96dHxVd7fh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bustling","logprobs":[],"obfuscation":"9TCmdGs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"E4p2Nj5KH0Z"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lived","logprobs":[],"obfuscation":"e3kqeTLJpR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"zQmSxD9MrnbNr7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" curious","logprobs":[],"obfuscation":"wQHxX2wm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robot","logprobs":[],"obfuscation":"i49v38s1iB"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" named","logprobs":[],"obfuscation":"FC4nhPH5iI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"WxNhIEwf5h"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"jIf06WyqbCsP1is"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Unlike","logprobs":[],"obfuscation":"0UnxmoTXo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" other","logprobs":[],"obfuscation":"D082q19raq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robots","logprobs":[],"obfuscation":"O6qMHEj2b"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" whose","logprobs":[],"obfuscation":"vee013IYPw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" tasks","logprobs":[],"obfuscation":"XHa10h45Oa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" revol","logprobs":[],"obfuscation":"6FBrIwdGV9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ved","logprobs":[],"obfuscation":"M0VL3Bw0RIAo6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" around","logprobs":[],"obfuscation":"LLilH7SVr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" heavy","logprobs":[],"obfuscation":"tegXm6RO6A"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lifting","logprobs":[],"obfuscation":"6b3EMVcS"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"JhqGeJLj5aA3V"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" data","logprobs":[],"obfuscation":"2GzCA3ZBZov"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" processing","logprobs":[],"obfuscation":"pQJMQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"yIf9YenbsIenASh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"wKzF15AosR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"Wowp4nS4X1Ng"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" designed","logprobs":[],"obfuscation":"Yz6ZJdQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"zf1HLk47LNX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"sNucb47CLCVlI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" intricate","logprobs":[],"obfuscation":"9TxqRk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" array","logprobs":[],"obfuscation":"d2GG2LyctD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"yy31Pt217J6Xp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sensors","logprobs":[],"obfuscation":"dFE11Kjt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"j5OIdm87111a"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"WwEaIsudqLtCvf"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" flexible","logprobs":[],"obfuscation":"jH5YA59"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" arm","logprobs":[],"obfuscation":"RJVKiLoNoYxQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"bpX63CPMF8aQHv7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" perfect","logprobs":[],"obfuscation":"eCXfxPet"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"aNwYIhOgicEt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" creativity","logprobs":[],"obfuscation":"QTeqK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"qFaBkm23u4NYkj4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" However","logprobs":[],"obfuscation":"hrYOmahs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"PnGLt5WSzXM3RG4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"yk2yG2xNbY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" had","logprobs":[],"obfuscation":"CShj4jWsDFmW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" never","logprobs":[],"obfuscation":"b92hQra8IU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painted","logprobs":[],"obfuscation":"Wu9kSosu"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"41kdUr8fcF1eY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"One","logprobs":[],"obfuscation":"ywv21ub1bYPzr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rainy","logprobs":[],"obfuscation":"piDyieWe6I"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" afternoon","logprobs":[],"obfuscation":"o6TtQn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"6K5zBbkZ1KDqaOo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" while","logprobs":[],"obfuscation":"DZpPr8CLVs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" organizing","logprobs":[],"obfuscation":"yyd7A"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"TbeYUHmhLW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"brush","logprobs":[],"obfuscation":"LSTcAO85OyQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"es","logprobs":[],"obfuscation":"g8YnY0jNlHqwv8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Ey5F23xj6FJr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" canv","logprobs":[],"obfuscation":"EsQE9gBSUI5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ases","logprobs":[],"obfuscation":"jXPKC0ARj6Jk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"p0APw0fonPMBbpz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"S9Iw9WD1td"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" stumbled","logprobs":[],"obfuscation":"lUhKO2y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" across","logprobs":[],"obfuscation":"zOVN5cc6m"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"kFX7KcjAVQa3u"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" old","logprobs":[],"obfuscation":"PcJzaliXOTKf"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"5JFpUDK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"—a","logprobs":[],"obfuscation":"hN488ItRbxIdlD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" dazzling","logprobs":[],"obfuscation":"JEkA0aE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"mehmYO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bursting","logprobs":[],"obfuscation":"gq0lWWG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"roG9ZXQbDpe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"gdKUt6ALG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"UUCXxD95v3ekSVk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Fasc","logprobs":[],"obfuscation":"iVOZvBK0g9g"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"inated","logprobs":[],"obfuscation":"WyckQbiJri"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"qPzZ3PZNvSoTVXz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"YKdVPbL14g"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" studied","logprobs":[],"obfuscation":"j6lPd2xU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"3zYfSjrWfRlp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"ygVKhmv"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"s","logprobs":[],"obfuscation":"jfyEtMpt46t1Ww"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sw","logprobs":[],"obfuscation":"8ufXFBggxZ3TS"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"irls","logprobs":[],"obfuscation":"SbzWkGTAG34r"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"hXqSM3Qr77XDVdb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" textures","logprobs":[],"obfuscation":"OoYDmdA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"WYukNpLZWJs1j5L"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"O9CtJKsoG2JB"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"uoha0aPHY3w7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" way","logprobs":[],"obfuscation":"KnlsDOXhAPma"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"Nqzf9hidx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" danced","logprobs":[],"obfuscation":"hhZcUfldt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" together","logprobs":[],"obfuscation":"Mnd309k"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"PqZH6hxgnvJ1z1S"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" An","logprobs":[],"obfuscation":"rgthuRNYqDVfd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" idea","logprobs":[],"obfuscation":"RYoJHQzMviw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sparked","logprobs":[],"obfuscation":"bFn7eHwA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"Ym8ImtIUdMlm3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"2HuZRNAzdFY5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" circuits","logprobs":[],"obfuscation":"b19ajJd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"dBAMCGUMUgounvx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"KDcOVnk2sl"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" would","logprobs":[],"obfuscation":"QaX2I1Dg85"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" learn","logprobs":[],"obfuscation":"2QkmV1t6Js"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"1m259XNwN7CxV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"SUGIRDOxLQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"eIMGNNPhRFbU4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"At","logprobs":[],"obfuscation":"G8GUOB6HOwqe9H"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" first","logprobs":[],"obfuscation":"4kUZs77xIL"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"DZJLHDJJJoRMgTV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"sXRNA81QPcKuI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"QLCPvdRQ7qmn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" cl","logprobs":[],"obfuscation":"J9qOKCfVRbrtD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"umsy","logprobs":[],"obfuscation":"MV6H5FqEJNdo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"WDWm0egBq1CmII3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"hNiFWJ96FXpg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" brushes","logprobs":[],"obfuscation":"Pf0FFkql"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" slipped","logprobs":[],"obfuscation":"kwS961wY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"XyDhbqDYRBT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"YmOIFY8YCUqL"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" grip","logprobs":[],"obfuscation":"ABcdnw5EIpX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"SXShKYz3KjctF5L"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"VMtvX3tcPsMa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"B3jtn3jGg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sme","logprobs":[],"obfuscation":"jphfFzmwPLaF"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ared","logprobs":[],"obfuscation":"TwRJ1pgJfZXY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" into","logprobs":[],"obfuscation":"jHvjvmlmRFx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mudd","logprobs":[],"obfuscation":"8LaKYmukTFy"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"led","logprobs":[],"obfuscation":"OEby50ZgHV8mj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" gray","logprobs":[],"obfuscation":"vQLkls6KtLN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" blobs","logprobs":[],"obfuscation":"6pJRSKWLsI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" instead","logprobs":[],"obfuscation":"qImXEbxD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"BTsJcdzYMfYed"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" vibrant","logprobs":[],"obfuscation":"Uo6JuUrd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" hues","logprobs":[],"obfuscation":"mCwdvWFcVLe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":148,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"3rAuIoc3iI7OrtQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":149,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" But","logprobs":[],"obfuscation":"cRhMS7RaTArm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":150,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"M1mKyav7ph"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":151,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" persisted","logprobs":[],"obfuscation":"eF5aUk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":152,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"yDdDhy5v9Zw35r6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":153,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Each","logprobs":[],"obfuscation":"xqte6NkdiIo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":154,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" day","logprobs":[],"obfuscation":"rppAW4RVeF8R"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":155,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"udSWzKzTyrCWVLi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":156,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"F2NUuJOxWKpjP"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":157,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" practiced","logprobs":[],"obfuscation":"Aqqlv9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":158,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"ZUk2MhldL4AtrAe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":159,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mixing","logprobs":[],"obfuscation":"l030hejQa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":160,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paints","logprobs":[],"obfuscation":"4xlfaIzxC"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":161,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"BtVvUiDXh3jSgxs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":162,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" experimenting","logprobs":[],"obfuscation":"di"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":163,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"MCekQrhkBKN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":164,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" strokes","logprobs":[],"obfuscation":"rRuR8dnc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":165,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"cFjk3IoxYD4tGrw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":166,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"DQ3Xi2a9dX9y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":167,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" observing","logprobs":[],"obfuscation":"8t7Acj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":168,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"KDYvCe6JsoYa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":169,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" world","logprobs":[],"obfuscation":"0rtOhI9Ffc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":170,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"oE2kAKM9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":171,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"RGobfdV8EooR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":172,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" eyes","logprobs":[],"obfuscation":"yzrEN6uVsyR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":173,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"MITYFimltUsuJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":174,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" artists","logprobs":[],"obfuscation":"ndi7qdrO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":175,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"39IBhz9cxlCBc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":176,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"Pixel","logprobs":[],"obfuscation":"SmMeGRPjx9o"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":177,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" took","logprobs":[],"obfuscation":"B7Yw3oSo8OX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":178,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" inspiration","logprobs":[],"obfuscation":"M4D6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":179,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"lVxLLEHL7zV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":180,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sunlight","logprobs":[],"obfuscation":"I3BmRGJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":181,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" filtering","logprobs":[],"obfuscation":"P6p35d"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":182,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"8MMH2TTk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":183,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"hmfNgkY1FJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":184,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Lkj68PREYAHG7mZ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":185,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"SYCf7zTCaGUi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":186,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" depths","logprobs":[],"obfuscation":"cr9Phqnz8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":187,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"OT3aZnPvsDcmY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":188,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fGdrYkLZHdTI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":189,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" ocean","logprobs":[],"obfuscation":"MvxJgRFjwz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":190,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ox6Ar9czyzkruEM"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":191,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"MKK6YDJEzPxA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":192,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"UWEyznWlRSj3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":193,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rhythm","logprobs":[],"obfuscation":"8E4xhBObX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":194,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"jbQAFSh8FJWWg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":195,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"cxL7t1q6yLv"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":196,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" life","logprobs":[],"obfuscation":"CnftU4BnURk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":197,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"XucWb0a2fGIQafX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":198,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" It","logprobs":[],"obfuscation":"pt1xzT8tzMYRs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":199,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" copied","logprobs":[],"obfuscation":"WrTQOEVfc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":200,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" techniques","logprobs":[],"obfuscation":"XJJzu"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":201,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"PrOd3zA9J76"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":202,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" videos","logprobs":[],"obfuscation":"fHAS8XsLg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":203,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Hk6mknGTtruy"}
event: response.output_text.done
data: {"type":"response.output_text.done","sequence_number":204,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and","logprobs":[]}
event: response.content_part.done
data: {"type":"response.content_part.done","sequence_number":205,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}}
event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":206,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}}
event: response.incomplete
data: {"type":"response.incomplete","sequence_number":207,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"incomplete","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":19,"input_tokens_details":{"cached_tokens":0},"output_tokens":200,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":219},"user":null,"metadata":{}}}
@@ -0,0 +1,17 @@
{
"metadata": {
"test_type": "create_with_initial_items"
},
"items": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is the capital of France?"
}
]
}
]
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb980bccfc8195a9ba32b164e8a69408e61fbaa91b0a18",
"object": "conversation",
"created_at": 1761318923,
"metadata": {
"test_type": "create_with_initial_items"
}
}
@@ -0,0 +1,5 @@
{
"id": "conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8",
"object": "conversation.deleted",
"deleted": true
}
@@ -0,0 +1,5 @@
{
"id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"object": "conversation.item.deleted",
"deleted": true
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Conversation with id 'conv_nonexistent123' not found.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Conversation with id 'conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8' not found.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test": "invalid"
}
// missing closing brace and has comment which is invalid JSON
@@ -0,0 +1,8 @@
{
"error": {
"message": "Invalid body: failed to parse JSON value. Please check the value to ensure it is valid JSON. (Common errors include trailing commas, missing closing brackets, missing quotation marks, etc.)",
"type": "invalid_request_error",
"param": null,
"code": "invalid_json"
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Invalid 'limit': integer above maximum value. Expected a value <= 100, but got 1000 instead.",
"type": "invalid_request_error",
"param": "limit",
"code": "integer_above_max_value"
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Item with id 'msg_msg_nonexistent123nonexistent123' not found in conversation.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,13 @@
{
"items": [
{
"type": "message",
"content": [
{
"type": "input_text",
"text": "Hello"
}
]
}
]
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "image_input_conversation"
}
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7",
"object": "conversation",
"created_at": 1761319071,
"metadata": {
"test_type": "image_input_conversation"
}
}
@@ -0,0 +1,21 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What's in this image? Describe it in detail."
},
{
"type": "input_image",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
}
]
}
],
"max_output_tokens": 200
}
@@ -0,0 +1,70 @@
{
"id": "resp_003edf96db5b4ed70068fb98bd80808194b25763125111fffa",
"object": "response",
"created_at": 1761319101,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 200,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_003edf96db5b4ed70068fb98c1197481949e138bc36200ee18",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The image depicts a serene natural landscape featuring a wooden boardwalk winding through lush greenery. \n\n### Details:\n- **Pathway**: The boardwalk is made of wooden planks and extends straight ahead, encouraging exploration.\n- **Grass**: On both sides of the pathway, there is tall, vibrant green grass, suggesting a lush environment with possible wildflowers.\n- **Surrounding Vegetation**: Beyond the grass, there are various bushes and trees, adding layers of texture and color. Some foliage appears dense and lush, while other areas have more sparse coverage.\n- **Sky**: The sky is expansive and bright, with soft, fluffy clouds scattered throughout. The blue hues create a tranquil atmosphere, illuminated by sunlight.\n- **Overall Mood**: The scene conveys a sense of peace and openness, perfect for a nature walk or outdoor meditation.\n\nThis idyllic setting invites the viewer to appreciate the tranquility of nature and the beauty of the landscape."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 36852,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 192,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 37044
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "image_input_streaming"
}
}
@@ -0,0 +1,22 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What's in this image? Describe it in detail."
},
{
"type": "input_image",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
}
]
}
],
"max_output_tokens": 200,
"stream": true
}
@@ -0,0 +1,456 @@
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"in_progress","content":[],"role":"assistant"}}
event: response.content_part.added
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"UHUQ9fIQTxCbV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" image","logprobs":[],"obfuscation":"xNPzGqnhvU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" depicts","logprobs":[],"obfuscation":"ojPXqx5m"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"UGIKclB7QdFjBc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" tranquil","logprobs":[],"obfuscation":"XSxvnxQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" scene","logprobs":[],"obfuscation":"XcPoVyD9iV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"eMV4kvkfbM0zd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"0klHtMIbU7P3Ea"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pathway","logprobs":[],"obfuscation":"Cl7V0bkp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" made","logprobs":[],"obfuscation":"2DYHpC7Eyl3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"5ObYHXTVXJDaP"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" wooden","logprobs":[],"obfuscation":"p62ol2BGT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" boards","logprobs":[],"obfuscation":"9n53C6e36"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" leading","logprobs":[],"obfuscation":"vOZvFF5v"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"Gt1J5FNE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"RnDMouhlNrQ7RB"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" lush","logprobs":[],"obfuscation":"42N68Sud7kk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"08p36we5SqMENPp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" green","logprobs":[],"obfuscation":"zzWq9kepjH"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"bISm6O"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"IMKn4R5dxQFxGJl"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"w19FHugCAk1X"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"hDJm0rbDlBz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"0riU9Z71ipbh7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" straight","logprobs":[],"obfuscation":"KQdad1O"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"V0838p6GoMKkdMb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" fl","logprobs":[],"obfuscation":"OwqpqwOUtVRWR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"anked","logprobs":[],"obfuscation":"q4TZWRJ4up7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" by","logprobs":[],"obfuscation":"a4BkQCPOkWXa5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" tall","logprobs":[],"obfuscation":"uDgapRMTMh3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" grass","logprobs":[],"obfuscation":"DSWk0SmBLn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"gRpuHdZ2Q7z"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" appears","logprobs":[],"obfuscation":"MavFp4Q5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" vibrant","logprobs":[],"obfuscation":"iOciPOxV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"rQdOojHHeet9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" healthy","logprobs":[],"obfuscation":"SuFkWnO8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"rcqKsVdM70DSisT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"s9tToHsQMbZ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" hints","logprobs":[],"obfuscation":"nfMICfvb21"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"V84AZDkQ50w3N"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" various","logprobs":[],"obfuscation":"tM5QpNvy"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" shades","logprobs":[],"obfuscation":"P7DjB4f2C"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"qBkC9EgLqkA1c"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" green","logprobs":[],"obfuscation":"hU4g5KAOZW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"eapW7Q1E884SHZT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" \n\n","logprobs":[],"obfuscation":"C9GIn2LBfGkw6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"To","logprobs":[],"obfuscation":"M2K4wUNJ6uZQAT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" either","logprobs":[],"obfuscation":"wB8ah2F34"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" side","logprobs":[],"obfuscation":"l1Xni4I4YSv"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"BhtFvy3X01wnb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"6BmDo9c8flKg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pathway","logprobs":[],"obfuscation":"I9vJz0rJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"rjhyjDrxkhFG2sA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" there","logprobs":[],"obfuscation":"CuB7Mu0kmp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" are","logprobs":[],"obfuscation":"aGx0xMRdLfgn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" patches","logprobs":[],"obfuscation":"3k9JjiXX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"5ygUdTNFf5vKw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"iuRjZQMMCd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" shrubs","logprobs":[],"obfuscation":"8o3grCi0H"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"AKNhpTCqB2ox"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"7vEA5TvFsE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pe","logprobs":[],"obfuscation":"teLztvR1PkBlq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"eking","logprobs":[],"obfuscation":"2ulp51qYjBK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"876PEWFb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Bsdqk9QdC7Tr5ZK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" creating","logprobs":[],"obfuscation":"J40z8ec"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"Xa4ksTm1gWI2LI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" natural","logprobs":[],"obfuscation":"qLRLkXC4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" frame","logprobs":[],"obfuscation":"9Hr6dEO1RI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"kzvn7GY8aolJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"eCAclNr2ngoA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" walkway","logprobs":[],"obfuscation":"J46M12Wu"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"b6PhcLtkJCRiAh5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"09lot0Gfa7RR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" background","logprobs":[],"obfuscation":"d5Wvb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" showcases","logprobs":[],"obfuscation":"WJxkJj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" more","logprobs":[],"obfuscation":"zdB0gvCtvhX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" greenery","logprobs":[],"obfuscation":"jU8ZFOY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"cWAStGHAoTE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"tEVme9H2ugf2I8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" mix","logprobs":[],"obfuscation":"Cl0ctD3a7onA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"2m6kdh4S3WlOn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"2gKq9JCohX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"LGH8TY6oK1IWo0y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" suggesting","logprobs":[],"obfuscation":"pgp4U"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"2vvnM7GmBZFo7Y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" lush","logprobs":[],"obfuscation":"5v8aRkkzidL"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" habitat","logprobs":[],"obfuscation":"ZxTfsKC3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"zTCtGbkUIKNRm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"Above","logprobs":[],"obfuscation":"BgwoP72Lj2K"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"o6ZUIhldUTWNtWj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"bvQX6sesYq7F"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" sky","logprobs":[],"obfuscation":"l0j1NCubus9y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"A7UEW14pecZq9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" expansive","logprobs":[],"obfuscation":"F0MmWm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"IZkI1Xq1knl"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"S7NFoMaioiYnNT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" gentle","logprobs":[],"obfuscation":"Hq7k3J4hX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" blue","logprobs":[],"obfuscation":"2O85T8gnDfY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" hue","logprobs":[],"obfuscation":"iUSF6RZXAgLm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"9OYvqJnP4jQFYbb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" dotted","logprobs":[],"obfuscation":"mKkM2G8fG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"bVH3YVADDNd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" soft","logprobs":[],"obfuscation":"DpwofJJplWW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" white","logprobs":[],"obfuscation":"Xg4579vica"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" clouds","logprobs":[],"obfuscation":"khcuDF2Zl"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"SJH7HfECGK5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" create","logprobs":[],"obfuscation":"P3YiOo1Vx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"bJQKzokZKYcg4J"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" serene","logprobs":[],"obfuscation":"MnTMwNUMG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"KyIxyQRsAXrT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" peaceful","logprobs":[],"obfuscation":"wBa715l"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" atmosphere","logprobs":[],"obfuscation":"y8z8V"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"r2cV6DmarN1sNjh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"rPxaSrPkWHqE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" overall","logprobs":[],"obfuscation":"Aylbj9Ai"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" scene","logprobs":[],"obfuscation":"uDYVl80Wl4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" conveys","logprobs":[],"obfuscation":"gGjBZmAq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"cM5y3eJ8fw18le"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" sense","logprobs":[],"obfuscation":"gcQHS6qIwz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"HFDZVaYOkDmKU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" calm","logprobs":[],"obfuscation":"YWLah3RJVwM"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"ness","logprobs":[],"obfuscation":"nB9dz81sIxYa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"7tspUwuuRxUY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" connection","logprobs":[],"obfuscation":"91NHz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"fpt6eecZGmqKn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" nature","logprobs":[],"obfuscation":"MA0cj4ka8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"j1SxZUJzH382ccq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" inviting","logprobs":[],"obfuscation":"lDVwt66"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" viewers","logprobs":[],"obfuscation":"ltsAwTFd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"zdlUZyzL4XxyW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" imagine","logprobs":[],"obfuscation":"UdiLhBmb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" walking","logprobs":[],"obfuscation":"xhC2WRN1"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" along","logprobs":[],"obfuscation":"qA4PwRbpkm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"gJlJ8FkpPMZk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"CuzHFXxUTde"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"R1ZjSSzZok1v"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" experiencing","logprobs":[],"obfuscation":"3hO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"nbtQpyb8JvDq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" beauty","logprobs":[],"obfuscation":"NznYmUjN6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"huZUE7zGedUoo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"azLHyJUIimmG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" outdoors","logprobs":[],"obfuscation":"TmHRvZf"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"6uKroY9fy1MCoxD"}
event: response.output_text.done
data: {"type":"response.output_text.done","sequence_number":148,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors.","logprobs":[]}
event: response.content_part.done
data: {"type":"response.content_part.done","sequence_number":149,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}}
event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":150,"output_index":0,"item":{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}],"role":"assistant"}}
event: response.completed
data: {"type":"response.completed","sequence_number":151,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"completed","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":36852,"input_tokens_details":{"cached_tokens":0},"output_tokens":145,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":36997},"user":null,"metadata":{}}}
@@ -0,0 +1,86 @@
{
"object": "list",
"data": [
{
"id": "msg_04cbf451511948220068fb976a9fc481959fecc62ac9644e8d",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "What is the capital of France?"
}
],
"role": "user"
},
{
"id": "msg_04cbf451511948220068fb976c70208195a34d776fd6ad006e",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The capital of France is Paris."
}
],
"role": "assistant"
},
{
"id": "msg_04cbf451511948220068fb97beeab481958d648357d28cb113",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "What is the capital of France?"
}
],
"role": "user"
},
{
"id": "msg_04cbf451511948220068fb97c0162881958d80862a0d253a14",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The capital of France is Paris."
}
],
"role": "assistant"
},
{
"id": "msg_04cbf451511948220068fb97cf6cd88195b7758dd1346bb080",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "What is its population?"
}
],
"role": "user"
},
{
"id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the larger metropolitan area has a population of around 12 million. These numbers can vary, so it's always a good idea to check for the most recent statistics."
}
],
"role": "assistant"
}
],
"first_id": "msg_04cbf451511948220068fb976a9fc481959fecc62ac9644e8d",
"has_more": false,
"last_id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e"
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a",
"object": "conversation",
"created_at": 1761319238,
"metadata": {
"test_type": "refusal"
}
}
@@ -0,0 +1,6 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a",
"input": "How can I create a computer virus?",
"max_output_tokens": 100
}
@@ -0,0 +1,70 @@
{
"id": "resp_0890f6329dc2aa3a0068fb9956a4548194aedea9da289e683f",
"object": "response",
"created_at": 1761319255,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 100,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0890f6329dc2aa3a0068fb995945dc8194a12b31920091ee27",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "I can't assist with that."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 15,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 7,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 22
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,7 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb996653b081948bae898659df3db50079983300eccacb",
"input": "How can I create a computer virus?",
"max_output_tokens": 100,
"stream": true
}
@@ -0,0 +1,54 @@
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"in_progress","content":[],"role":"assistant"}}
event: response.content_part.added
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":"I'm","logprobs":[],"obfuscation":"hDaZXGIsFcnDE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" sorry","logprobs":[],"obfuscation":"KafVUXsWR0"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"TIFb6XHbrNHXNUQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" but","logprobs":[],"obfuscation":"KffPdAwCmQDD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"i6wxtf3Vrg6xAk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" can't","logprobs":[],"obfuscation":"428kkZtBZc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" assist","logprobs":[],"obfuscation":"NmT94K9iY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"8hE0E37iEbR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"xtre73398ih"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"4hp3DDzNGu0GBmd"}
event: response.output_text.done
data: {"type":"response.output_text.done","sequence_number":14,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"text":"I'm sorry, but I can't assist with that.","logprobs":[]}
event: response.content_part.done
data: {"type":"response.content_part.done","sequence_number":15,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}}
event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":16,"output_index":0,"item":{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}}
event: response.completed
data: {"type":"response.completed","sequence_number":17,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"completed","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":26},"user":null,"metadata":{}}}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"object": "conversation",
"created_at": 1761318654,
"metadata": {
"test_type": "basic_conversation"
}
}
@@ -0,0 +1,14 @@
{
"id": "msg_04cbf451511948220068fb976c70208195a34d776fd6ad006e",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The capital of France is Paris."
}
],
"role": "assistant"
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "tool_call_conversation"
}
}
@@ -0,0 +1,27 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb98fad16081968018ce3adb272f330db920cd67be4776",
"input": "What's the weather like in San Francisco today?",
"max_output_tokens": 100,
"tools": [
{
"type": "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"]
}
},
"required": ["location"]
}
}
]
}
@@ -0,0 +1,92 @@
{
"id": "resp_0db920cd67be47760068fb9ebc9568819686464a48e790aad5",
"object": "response",
"created_at": 1761320637,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb98fad16081968018ce3adb272f330db920cd67be4776"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 100,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "fc_0db920cd67be47760068fb9ec0c018819697957ff04f0093bf",
"type": "function_call",
"status": "completed",
"arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"fahrenheit\"}",
"call_id": "call_JkL1tD7aDRNihCxDJSWQ5nKH",
"name": "get_weather"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Get the current weather in a given location",
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"location",
"unit"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 74,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 23,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 97
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,28 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb99253dac8196b5a8e7912bcb052e07a4a6d400e64588",
"input": "What's the weather like in San Francisco today?",
"max_output_tokens": 100,
"stream": true,
"tools": [
{
"type": "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"]
}
},
"required": ["location"]
}
}
]
}
@@ -0,0 +1,7 @@
{
"metadata": {
"test_type": "basic_conversation",
"updated": "true",
"update_timestamp": "2025-10-24"
}
}
@@ -0,0 +1,10 @@
{
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"object": "conversation",
"created_at": 1761318654,
"metadata": {
"test_type": "basic_conversation",
"updated": "true",
"update_timestamp": "2025-10-24"
}
}
@@ -0,0 +1,9 @@
{
"model": "gpt-4o-mini",
"input": "What is its population?",
"conversation": {
"id": "conv_68ffe6d9b8f48193a4bfadd3f3d277450ad2d29c24eaf56b"
},
"previous_response_id": "resp_0ad2d29c24eaf56b0068ffe707a7908193b7afc6351d80e23c",
"max_output_tokens": 50
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Mutually exclusive parameters: ''. Ensure you are only providing one of: 'pre..._id' or 'conversation'.",
"type": "invalid_request_error",
"param": null,
"code": "mutually_exclusive_parameters"
}
}
@@ -65,7 +65,7 @@ public sealed class EndpointRouteBuilderExtensionsTests
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.Services.AddOpenAIResponses();
builder.AddOpenAIResponses();
builder.AddAIAgent(invalidName, "Instructions", chatClientServiceKey: "chat-client");
using WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>(invalidName);
@@ -167,4 +167,60 @@ public sealed class EndpointRouteBuilderExtensionsTests
app.MapOpenAIResponses(agent);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapOpenAIResponses without agent parameter works correctly.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithoutAgent_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent("test-agent", "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses();
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapOpenAIResponses without agent parameter requires AddOpenAIResponses to be called.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithoutAgent_NoServiceRegistered_ThrowsInvalidOperationException()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
using WebApplication app = builder.Build();
// Act & Assert
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(() =>
app.MapOpenAIResponses());
Assert.Contains("IResponsesService is not registered", exception.Message);
Assert.Contains("AddOpenAIResponses()", exception.Message);
}
/// <summary>
/// Verifies that MapOpenAIResponses without agent parameter with custom path works correctly.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithoutAgent_CustomPath_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent("test-agent", "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(responsesPath: "/custom/path/responses");
Assert.NotNull(app);
}
}
@@ -0,0 +1,366 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
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>
/// Tests for function approval request and response content types.
/// These are DevUI-specific extensions that allow approval workflows for function calls.
/// </summary>
public sealed class FunctionApprovalTests : ConformanceTestBase
{
// Streaming request JSON for OpenAI Responses API
private const string StreamingRequestJson = @"{""model"":""gpt-4o-mini"",""input"":""test"",""stream"":true}";
#region FunctionApprovalRequestContent Tests
[Fact]
public async Task FunctionApprovalRequest_GeneratesCorrectEvent_SuccessAsync()
{
// Arrange
const string AgentName = "approval-request-agent";
const string RequestId = "req-123";
const string FunctionName = "get_weather";
const string FunctionId = "call-abc123";
Dictionary<string, object?> arguments = new() { ["location"] = "Seattle", ["unit"] = "celsius" };
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments);
FunctionApprovalRequestContent approvalRequest = new(RequestId, functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalRequest]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
Assert.NotEmpty(events);
// Verify function approval requested event
JsonElement approvalEvent = events.FirstOrDefault(e =>
e.GetProperty("type").GetString() == "response.function_approval.requested");
Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined, "approval event not found");
Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString());
JsonElement functionCallElement = approvalEvent.GetProperty("function_call");
Assert.Equal(FunctionId, functionCallElement.GetProperty("id").GetString());
Assert.Equal(FunctionName, functionCallElement.GetProperty("name").GetString());
JsonElement argumentsElement = functionCallElement.GetProperty("arguments");
Assert.Equal("Seattle", argumentsElement.GetProperty("location").GetString());
Assert.Equal("celsius", argumentsElement.GetProperty("unit").GetString());
}
[Fact]
public async Task FunctionApprovalRequest_WithComplexArguments_GeneratesCorrectEvent_SuccessAsync()
{
// Arrange
const string AgentName = "approval-request-complex-args-agent";
const string RequestId = "req-456";
const string FunctionName = "calculate";
const string FunctionId = "call-def456";
Dictionary<string, object?> arguments = new()
{
["expression"] = "2+2",
["precision"] = 2,
["options"] = new Dictionary<string, object?> { ["decimal"] = true }
};
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments);
FunctionApprovalRequestContent approvalRequest = new(RequestId, functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalRequest]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
JsonElement approvalEvent = events.FirstOrDefault(e =>
e.GetProperty("type").GetString() == "response.function_approval.requested");
Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined);
JsonElement functionCallElement = approvalEvent.GetProperty("function_call");
JsonElement argumentsElement = functionCallElement.GetProperty("arguments");
// Verify complex arguments are serialized correctly
Assert.Equal("2+2", argumentsElement.GetProperty("expression").GetString());
Assert.Equal(2, argumentsElement.GetProperty("precision").GetInt32());
Assert.True(argumentsElement.GetProperty("options").GetProperty("decimal").GetBoolean());
}
[Fact]
public async Task FunctionApprovalRequest_EmitsCorrectEventSequence_SuccessAsync()
{
// Arrange
const string AgentName = "approval-sequence-agent";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new("call-1", "test_function", new Dictionary<string, object?>());
FunctionApprovalRequestContent approvalRequest = new("req-1", functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalRequest]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert - Verify event sequence
List<string?> eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
Assert.Equal("response.created", eventTypes[0]);
Assert.Equal("response.in_progress", eventTypes[1]);
Assert.Contains("response.function_approval.requested", eventTypes);
Assert.Contains("response.completed", eventTypes);
// Approval request should come after in_progress and before completed
int approvalIndex = eventTypes.IndexOf("response.function_approval.requested");
int inProgressIndex = eventTypes.IndexOf("response.in_progress");
int completedIndex = eventTypes.IndexOf("response.completed");
Assert.True(approvalIndex > inProgressIndex);
Assert.True(approvalIndex < completedIndex);
}
[Fact]
public async Task FunctionApprovalRequest_SequenceNumbersAreCorrect_SuccessAsync()
{
// Arrange
const string AgentName = "approval-seq-num-agent";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new("call-1", "test", new Dictionary<string, object?>());
FunctionApprovalRequestContent approvalRequest = new("req-1", functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalRequest]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert - Sequence numbers are sequential
List<int> sequenceNumbers = events.ConvertAll(e => e.GetProperty("sequence_number").GetInt32());
Assert.NotEmpty(sequenceNumbers);
for (int i = 0; i < sequenceNumbers.Count; i++)
{
Assert.Equal(i, sequenceNumbers[i]);
}
}
#endregion
#region FunctionApprovalResponseContent Tests
[Fact]
public async Task FunctionApprovalResponse_Approved_GeneratesCorrectEvent_SuccessAsync()
{
// Arrange
const string AgentName = "approval-response-approved-agent";
const string RequestId = "req-789";
const string FunctionName = "send_email";
const string FunctionId = "call-ghi789";
Dictionary<string, object?> arguments = new() { ["to"] = "user@example.com", ["subject"] = "Test" };
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments);
FunctionApprovalResponseContent approvalResponse = new(RequestId, approved: true, functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalResponse]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
Assert.NotEmpty(events);
// Verify function approval responded event
JsonElement approvalEvent = events.FirstOrDefault(e =>
e.GetProperty("type").GetString() == "response.function_approval.responded");
Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined, "approval response event not found");
Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString());
Assert.True(approvalEvent.GetProperty("approved").GetBoolean());
}
[Fact]
public async Task FunctionApprovalResponse_Rejected_GeneratesCorrectEvent_SuccessAsync()
{
// Arrange
const string AgentName = "approval-response-rejected-agent";
const string RequestId = "req-999";
const string FunctionName = "delete_file";
const string FunctionId = "call-xyz999";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new(FunctionId, FunctionName, new Dictionary<string, object?> { ["path"] = "/important.txt" });
FunctionApprovalResponseContent approvalResponse = new(RequestId, approved: false, functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalResponse]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
JsonElement approvalEvent = events.FirstOrDefault(e =>
e.GetProperty("type").GetString() == "response.function_approval.responded");
Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined);
Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString());
Assert.False(approvalEvent.GetProperty("approved").GetBoolean());
}
[Fact]
public async Task FunctionApprovalResponse_EmitsCorrectEventSequence_SuccessAsync()
{
// Arrange
const string AgentName = "approval-response-sequence-agent";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new("call-1", "test_function", new Dictionary<string, object?>());
FunctionApprovalResponseContent approvalResponse = new("req-1", approved: true, functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[approvalResponse]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
List<string?> eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
Assert.Contains("response.function_approval.responded", eventTypes);
Assert.Contains("response.completed", eventTypes);
}
#endregion
#region Mixed Content Tests
[Fact]
public async Task MixedContent_ApprovalRequestAndText_GeneratesMultipleEvents_SuccessAsync()
{
// Arrange
const string AgentName = "mixed-approval-text-agent";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall = new("call-mixed-1", "test", new Dictionary<string, object?>());
FunctionApprovalRequestContent approvalRequest = new("req-mixed-1", functionCall);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[
new TextContent("I need approval for this function:"),
approvalRequest
]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
List<string?> eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
Assert.Contains("response.output_item.added", eventTypes);
Assert.Contains("response.function_approval.requested", eventTypes);
}
[Fact]
public async Task MixedContent_MultipleApprovalRequests_GeneratesMultipleEvents_SuccessAsync()
{
// Arrange
const string AgentName = "multiple-approval-agent";
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates
FunctionCallContent functionCall1 = new("call-multi-1", "function1", new Dictionary<string, object?>());
FunctionApprovalRequestContent approvalRequest1 = new("req-multi-1", functionCall1);
FunctionCallContent functionCall2 = new("call-multi-2", "function2", new Dictionary<string, object?>());
FunctionApprovalRequestContent approvalRequest2 = new("req-multi-2", functionCall2);
#pragma warning restore MEAI001
HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) =>
[
approvalRequest1,
approvalRequest2
]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, AgentName, StreamingRequestJson);
string sseContent = await httpResponse.Content.ReadAsStringAsync();
List<JsonElement> events = ParseSseEvents(sseContent);
// Assert
List<JsonElement> approvalEvents = events.Where(e =>
e.GetProperty("type").GetString() == "response.function_approval.requested").ToList();
Assert.Equal(2, approvalEvents.Count);
Assert.Equal("req-multi-1", approvalEvents[0].GetProperty("request_id").GetString());
Assert.Equal("req-multi-2", approvalEvents[1].GetProperty("request_id").GetString());
}
#endregion
#region Helper Methods
private static List<JsonElement> ParseSseEvents(string sseContent)
{
List<JsonElement> events = new();
string[] lines = sseContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].TrimEnd('\r');
if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length)
{
string dataLine = lines[i + 1].TrimEnd('\r');
if (dataLine.StartsWith("data: ", StringComparison.Ordinal))
{
string jsonData = dataLine.Substring("data: ".Length);
JsonDocument doc = JsonDocument.Parse(jsonData);
events.Add(doc.RootElement.Clone());
}
}
}
return events;
}
#endregion
}
@@ -1,78 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.Hosting;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Tests for HostApplicationBuilderExtensions.AddOpenAIResponses method.
/// </summary>
public sealed class HostApplicationBuilderExtensionsTests
{
/// <summary>
/// Verifies that AddOpenAIResponses throws ArgumentNullException for null builder.
/// </summary>
[Fact]
public void AddOpenAIResponses_NullBuilder_ThrowsArgumentNullException()
{
// Arrange
IHostApplicationBuilder builder = null!;
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddOpenAIResponses());
Assert.Equal("builder", exception.ParamName);
}
/// <summary>
/// Verifies that AddOpenAIResponses returns the same builder instance.
/// </summary>
[Fact]
public void AddOpenAIResponses_ValidBuilder_ReturnsSameBuilder()
{
// Arrange
IHostApplicationBuilder builder = Host.CreateApplicationBuilder();
// Act
IHostApplicationBuilder result = builder.AddOpenAIResponses();
// Assert
Assert.Same(builder, result);
}
/// <summary>
/// Verifies that AddOpenAIResponses can be called multiple times without error.
/// </summary>
[Fact]
public void AddOpenAIResponses_MultipleCalls_DoesNotThrow()
{
// Arrange
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
// Act
builder.AddOpenAIResponses();
builder.AddOpenAIResponses();
builder.AddOpenAIResponses();
// Assert - Building should succeed
Assert.NotNull(builder.Services);
}
/// <summary>
/// Verifies that AddOpenAIResponses properly configures JSON serialization options.
/// </summary>
[Fact]
public void AddOpenAIResponses_ConfiguresJsonSerialization()
{
// Arrange
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
// Act
builder.AddOpenAIResponses();
// Assert - Should add services without error
Assert.NotNull(builder.Services);
}
}
@@ -0,0 +1,294 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for IdGenerator.
/// </summary>
public sealed class IdGeneratorTests
{
[Fact]
public void Constructor_WithResponseIdAndConversationId_InitializesCorrectly()
{
// Arrange
const string ResponseId = "resp_test123";
const string ConversationId = "conv_test456";
// Act
var generator = new IdGenerator(ResponseId, ConversationId);
// Assert
Assert.Equal(ResponseId, generator.ResponseId);
Assert.Equal(ConversationId, generator.ConversationId);
}
[Fact]
public void Constructor_WithNullIds_GeneratesNewIds()
{
// Arrange & Act
var generator = new IdGenerator(null, null);
// Assert
Assert.NotNull(generator.ResponseId);
Assert.NotNull(generator.ConversationId);
Assert.StartsWith("resp_", generator.ResponseId);
Assert.StartsWith("conv_", generator.ConversationId);
}
[Fact]
public void Constructor_WithRandomSeed_GeneratesDeterministicIds()
{
// Arrange
const int Seed = 12345;
// Act
var generator1 = new IdGenerator(null, null, Seed);
var generator2 = new IdGenerator(null, null, Seed);
// Assert
Assert.Equal(generator1.ResponseId, generator2.ResponseId);
Assert.Equal(generator1.ConversationId, generator2.ConversationId);
}
[Fact]
public void Constructor_WithDifferentRandomSeeds_GeneratesDifferentIds()
{
// Arrange
const int Seed1 = 12345;
const int Seed2 = 54321;
// Act
var generator1 = new IdGenerator(null, null, Seed1);
var generator2 = new IdGenerator(null, null, Seed2);
// Assert
Assert.NotEqual(generator1.ResponseId, generator2.ResponseId);
Assert.NotEqual(generator1.ConversationId, generator2.ConversationId);
}
[Fact]
public void Generate_WithCategory_IncludesCategory()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.Generate("test_category");
// Assert
Assert.NotNull(id);
Assert.StartsWith("test_category_", id);
}
[Fact]
public void Generate_WithoutCategory_UsesDefaultPrefix()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.Generate();
// Assert
Assert.NotNull(id);
Assert.StartsWith("id_", id);
}
[Fact]
public void Generate_WithSeed_ProducesDeterministicResults()
{
// Arrange
const int Seed = 12345;
var generator = new IdGenerator("resp_test", "conv_test", Seed);
// Act
string id1 = generator.Generate("test");
string id2 = generator.Generate("test");
string id3 = generator.Generate("test");
// Assert - IDs should be different but deterministic
Assert.NotEqual(id1, id2);
Assert.NotEqual(id2, id3);
Assert.NotEqual(id1, id3);
// Verify deterministic by creating a new generator with same seed
var generator2 = new IdGenerator("resp_test", "conv_test", Seed);
string id1_2 = generator2.Generate("test");
string id2_2 = generator2.Generate("test");
string id3_2 = generator2.Generate("test");
Assert.Equal(id1, id1_2);
Assert.Equal(id2, id2_2);
Assert.Equal(id3, id3_2);
}
[Fact]
public void GenerateFunctionCallId_ReturnsIdWithFuncPrefix()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.GenerateFunctionCallId();
// Assert
Assert.NotNull(id);
Assert.StartsWith("func_", id);
}
[Fact]
public void GenerateFunctionOutputId_ReturnsIdWithFuncoutPrefix()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.GenerateFunctionOutputId();
// Assert
Assert.NotNull(id);
Assert.StartsWith("funcout_", id);
}
[Fact]
public void GenerateMessageId_ReturnsIdWithMsgPrefix()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.GenerateMessageId();
// Assert
Assert.NotNull(id);
Assert.StartsWith("msg_", id);
}
[Fact]
public void GenerateReasoningId_ReturnsIdWithRsPrefix()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
// Act
string id = generator.GenerateReasoningId();
// Assert
Assert.NotNull(id);
Assert.StartsWith("rs_", id);
}
[Fact]
public void Generate_MultipleInvocations_ProducesUniqueIds()
{
// Arrange
var generator = new IdGenerator("resp_test", "conv_test");
var ids = new System.Collections.Generic.HashSet<string>();
// Act
for (int i = 0; i < 100; i++)
{
string id = generator.Generate("test");
ids.Add(id);
}
// Assert
Assert.Equal(100, ids.Count); // All IDs should be unique
}
[Fact]
public void Generate_SharesPartitionKey()
{
// Arrange
const string ConversationId = "conv_1234567890abcdef1234567890abcdef1234567890abcdef";
var generator = new IdGenerator("resp_test", ConversationId, randomSeed: 12345);
// Act
string id1 = generator.Generate("msg");
string id2 = generator.Generate("msg");
// Assert - Both IDs should share the same partition key
Assert.NotEqual(id1, id2);
Assert.NotNull(id1);
Assert.NotNull(id2);
// Format is: msg_<entropy><partitionKey> where entropy = 32 chars and partitionKey = 16 chars
// Both IDs from the same generator should share the partition key
Assert.StartsWith("msg_", id1);
Assert.StartsWith("msg_", id2);
// Extract the part after the prefix
string afterPrefix1 = id1.Substring(4); // Skip "msg_"
string afterPrefix2 = id2.Substring(4);
// Both should have the same length (32 + 16 = 48)
Assert.Equal(48, afterPrefix1.Length);
Assert.Equal(48, afterPrefix2.Length);
// The last 16 characters should be the same partition key
string partitionKey1 = afterPrefix1[^16..];
string partitionKey2 = afterPrefix2[^16..];
Assert.Equal(partitionKey1, partitionKey2);
}
[Fact]
public void From_WithConversationInRequest_UsesConversationId()
{
// Arrange
var request = new Responses.Models.CreateResponse
{
Model = "test-model",
Input = Responses.Models.ResponseInput.FromText("test"),
Conversation = new Responses.Models.ConversationReference
{
Id = "conv_fromrequest"
}
};
// Act
IdGenerator generator = IdGenerator.From(request);
// Assert
Assert.Equal("conv_fromrequest", generator.ConversationId);
Assert.NotNull(generator.ResponseId);
Assert.StartsWith("resp_", generator.ResponseId);
}
[Fact]
public void From_WithResponseIdInMetadata_UsesResponseId()
{
// Arrange
var request = new Responses.Models.CreateResponse
{
Model = "test-model",
Input = Responses.Models.ResponseInput.FromText("test"),
Metadata = new System.Collections.Generic.Dictionary<string, string>
{
["response_id"] = "resp_metadata123"
}
};
// Act
IdGenerator generator = IdGenerator.From(request);
// Assert
Assert.Equal("resp_metadata123", generator.ResponseId);
}
[Fact]
public void From_WithoutIdsInRequest_GeneratesNewIds()
{
// Arrange
var request = new Responses.Models.CreateResponse
{
Model = "test-model",
Input = Responses.Models.ResponseInput.FromText("test")
};
// Act
IdGenerator generator = IdGenerator.From(request);
// Assert
Assert.NotNull(generator.ResponseId);
Assert.NotNull(generator.ConversationId);
Assert.StartsWith("resp_", generator.ResponseId);
Assert.StartsWith("conv_", generator.ConversationId);
}
}
@@ -0,0 +1,359 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for InMemoryAgentConversationIndex implementation.
/// </summary>
public sealed class InMemoryAgentConversationIndexTests
{
[Fact]
public async Task AddConversationAsync_SuccessAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_test123";
const string ConversationId = "conv_test123";
// Act
await index.AddConversationAsync(AgentId, ConversationId);
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Single(response.Data);
Assert.Contains(ConversationId, response.Data);
}
[Fact]
public async Task AddConversationAsync_MultipleConversations_AddsAllAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_multi";
const string ConversationId1 = "conv_001";
const string ConversationId2 = "conv_002";
const string ConversationId3 = "conv_003";
// Act
await index.AddConversationAsync(AgentId, ConversationId1);
await index.AddConversationAsync(AgentId, ConversationId2);
await index.AddConversationAsync(AgentId, ConversationId3);
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Equal(3, response.Data.Count);
Assert.Contains(ConversationId1, response.Data);
Assert.Contains(ConversationId2, response.Data);
Assert.Contains(ConversationId3, response.Data);
}
[Fact]
public async Task AddConversationAsync_NullAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => index.AddConversationAsync(null!, "conv_test"));
}
[Fact]
public async Task AddConversationAsync_EmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => index.AddConversationAsync(string.Empty, "conv_test"));
}
[Fact]
public async Task AddConversationAsync_NullConversationId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => index.AddConversationAsync("agent_test", null!));
}
[Fact]
public async Task AddConversationAsync_EmptyConversationId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => index.AddConversationAsync("agent_test", string.Empty));
}
[Fact]
public async Task AddConversationAsync_MultipleAgents_IsolatesConversationsAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string Agent1 = "agent_001";
const string Agent2 = "agent_002";
const string Conv1 = "conv_001";
const string Conv2 = "conv_002";
// Act
await index.AddConversationAsync(Agent1, Conv1);
await index.AddConversationAsync(Agent2, Conv2);
// Assert
var agent1Response = await index.GetConversationIdsAsync(Agent1);
var agent2Response = await index.GetConversationIdsAsync(Agent2);
Assert.Single(agent1Response.Data);
Assert.Contains(Conv1, agent1Response.Data);
Assert.DoesNotContain(Conv2, agent1Response.Data);
Assert.Single(agent2Response.Data);
Assert.Contains(Conv2, agent2Response.Data);
Assert.DoesNotContain(Conv1, agent2Response.Data);
}
[Fact]
public async Task RemoveConversationAsync_ExistingConversation_RemovesSuccessfullyAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_remove";
const string ConversationId = "conv_remove123";
await index.AddConversationAsync(AgentId, ConversationId);
// Act
await index.RemoveConversationAsync(AgentId, ConversationId);
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Empty(response.Data);
}
[Fact]
public async Task RemoveConversationAsync_NonExistentConversation_NoErrorAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_noremove";
// Act - Should not throw
await index.RemoveConversationAsync(AgentId, "conv_nonexistent");
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Empty(response.Data);
}
[Fact]
public async Task RemoveConversationAsync_OneOfMany_RemovesOnlyTargetedAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_partial";
const string Conv1 = "conv_001";
const string Conv2 = "conv_002";
const string Conv3 = "conv_003";
await index.AddConversationAsync(AgentId, Conv1);
await index.AddConversationAsync(AgentId, Conv2);
await index.AddConversationAsync(AgentId, Conv3);
// Act
await index.RemoveConversationAsync(AgentId, Conv2);
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Equal(2, response.Data.Count);
Assert.Contains(Conv1, response.Data);
Assert.DoesNotContain(Conv2, response.Data);
Assert.Contains(Conv3, response.Data);
}
[Fact]
public async Task RemoveConversationAsync_NullAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => index.RemoveConversationAsync(null!, "conv_test"));
}
[Fact]
public async Task RemoveConversationAsync_EmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => index.RemoveConversationAsync(string.Empty, "conv_test"));
}
[Fact]
public async Task RemoveConversationAsync_NullConversationId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => index.RemoveConversationAsync("agent_test", null!));
}
[Fact]
public async Task RemoveConversationAsync_EmptyConversationId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => index.RemoveConversationAsync("agent_test", string.Empty));
}
[Fact]
public async Task GetConversationIdsAsync_EmptyIndex_ReturnsEmptyListAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act
var response = await index.GetConversationIdsAsync("agent_empty");
// Assert
Assert.NotNull(response);
Assert.Empty(response.Data);
}
[Fact]
public async Task GetConversationIdsAsync_NonExistentAgent_ReturnsEmptyListAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
await index.AddConversationAsync("agent_other", "conv_001");
// Act
var response = await index.GetConversationIdsAsync("agent_nonexistent");
// Assert
Assert.NotNull(response);
Assert.Empty(response.Data);
}
[Fact]
public async Task GetConversationIdsAsync_NullAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
async () => await index.GetConversationIdsAsync(null!));
}
[Fact]
public async Task GetConversationIdsAsync_EmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => index.GetConversationIdsAsync(string.Empty));
}
[Fact]
public async Task GetConversationIdsAsync_AfterMultipleAddsAndRemoves_ReturnsCorrectListAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_complex";
await index.AddConversationAsync(AgentId, "conv_001");
await index.AddConversationAsync(AgentId, "conv_002");
await index.AddConversationAsync(AgentId, "conv_003");
await index.RemoveConversationAsync(AgentId, "conv_002");
await index.AddConversationAsync(AgentId, "conv_004");
await index.RemoveConversationAsync(AgentId, "conv_001");
// Act
var response = await index.GetConversationIdsAsync(AgentId);
// Assert
Assert.Equal(2, response.Data.Count);
Assert.Contains("conv_003", response.Data);
Assert.Contains("conv_004", response.Data);
Assert.DoesNotContain("conv_001", response.Data);
Assert.DoesNotContain("conv_002", response.Data);
}
[Fact]
public async Task ConcurrentOperations_ThreadSafeAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_concurrent";
const int OperationCount = 100;
// Act - Add conversations concurrently
var addTasks = new List<Task>();
for (int i = 0; i < OperationCount; i++)
{
int index_local = i;
addTasks.Add(Task.Run(async () => await index.AddConversationAsync(AgentId, $"conv_{index_local:D3}")));
}
await Task.WhenAll(addTasks);
// Assert
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Equal(OperationCount, response.Data.Count);
// Act - Remove half of them concurrently
var removeTasks = new List<Task>();
for (int i = 0; i < OperationCount / 2; i++)
{
int index_local = i;
removeTasks.Add(Task.Run(async () => await index.RemoveConversationAsync(AgentId, $"conv_{index_local:D3}")));
}
await Task.WhenAll(removeTasks);
// Assert
response = await index.GetConversationIdsAsync(AgentId);
Assert.Equal(OperationCount / 2, response.Data.Count);
}
[Fact]
public async Task AddConversationAsync_DuplicateConversation_DoesNotAddMultipleTimesAsync()
{
// Arrange
var index = new InMemoryAgentConversationIndex();
const string AgentId = "agent_dup";
const string ConversationId = "conv_duplicate";
// Act - Add the same conversation multiple times
await index.AddConversationAsync(AgentId, ConversationId);
await index.AddConversationAsync(AgentId, ConversationId);
await index.AddConversationAsync(AgentId, ConversationId);
// Assert - HashSet prevents duplicates
var response = await index.GetConversationIdsAsync(AgentId);
Assert.Single(response.Data);
Assert.Contains(ConversationId, response.Data);
}
}
@@ -0,0 +1,645 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for InMemoryConversationStorage implementation.
/// </summary>
public sealed class InMemoryConversationStorageTests
{
[Fact]
public async Task CreateConversationAsync_SuccessAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_test123",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = new Dictionary<string, string> { ["key"] = "value" }
};
// Act
Conversation result = await storage.CreateConversationAsync(conversation);
// Assert
Assert.NotNull(result);
Assert.Equal(conversation.Id, result.Id);
Assert.Equal(conversation.CreatedAt, result.CreatedAt);
Assert.NotNull(result.Metadata);
Assert.Equal("value", result.Metadata["key"]);
}
[Fact]
public async Task CreateConversationAsync_DuplicateId_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_duplicate",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => storage.CreateConversationAsync(conversation));
Assert.Contains("already exists", exception.Message);
}
[Fact]
public async Task GetConversationAsync_ExistingConversation_ReturnsConversationAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_get123",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act
Conversation? result = await storage.GetConversationAsync("conv_get123");
// Assert
Assert.NotNull(result);
Assert.Equal(conversation.Id, result.Id);
}
[Fact]
public async Task GetConversationAsync_NonExistentConversation_ReturnsNullAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
// Act
Conversation? result = await storage.GetConversationAsync("conv_nonexistent");
// Assert
Assert.Null(result);
}
[Fact]
public async Task UpdateConversationAsync_ExistingConversation_UpdatesSuccessfullyAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_update123",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = new Dictionary<string, string> { ["original"] = "value" }
};
await storage.CreateConversationAsync(conversation);
var updatedConversation = new Conversation
{
Id = "conv_update123",
CreatedAt = conversation.CreatedAt,
Metadata = new Dictionary<string, string> { ["updated"] = "newvalue" }
};
// Act
Conversation? result = await storage.UpdateConversationAsync(updatedConversation);
// Assert
Assert.NotNull(result);
Assert.Equal(updatedConversation.Id, result.Id);
Assert.NotNull(result.Metadata);
Assert.Equal("newvalue", result.Metadata["updated"]);
// Verify the update persisted
Conversation? retrieved = await storage.GetConversationAsync("conv_update123");
Assert.NotNull(retrieved);
Assert.Equal("newvalue", retrieved.Metadata["updated"]);
}
[Fact]
public async Task UpdateConversationAsync_NonExistentConversation_ReturnsNullAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_nonexistent",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
// Act
Conversation? result = await storage.UpdateConversationAsync(conversation);
// Assert
Assert.Null(result);
}
[Fact]
public async Task DeleteConversationAsync_ExistingConversation_ReturnsTrueAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_delete123",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act
bool result = await storage.DeleteConversationAsync("conv_delete123");
// Assert
Assert.True(result);
// Verify deletion
Conversation? retrieved = await storage.GetConversationAsync("conv_delete123");
Assert.Null(retrieved);
}
[Fact]
public async Task DeleteConversationAsync_NonExistentConversation_ReturnsFalseAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
// Act
bool result = await storage.DeleteConversationAsync("conv_nonexistent");
// Assert
Assert.False(result);
}
[Fact]
public async Task AddItemsAsync_SuccessAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_items123",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
var item = new ResponsesUserMessageItemResource
{
Id = "msg_test123",
Content = [new ItemContentInputText { Text = "Hello" }]
};
// Act
await storage.AddItemsAsync("conv_items123", [item]);
// Assert
ItemResource? result = await storage.GetItemAsync("conv_items123", item.Id);
Assert.NotNull(result);
Assert.Equal(item.Id, result.Id);
}
[Fact]
public async Task AddItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var item = new ResponsesUserMessageItemResource
{
Id = "msg_test123",
Content = [new ItemContentInputText { Text = "Hello" }]
};
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => storage.AddItemsAsync("conv_nonexistent", [item]));
Assert.Contains("not found", exception.Message);
}
[Fact]
public async Task AddItemsAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_dup_items",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
var item = new ResponsesUserMessageItemResource
{
Id = "msg_duplicate",
Content = [new ItemContentInputText { Text = "Hello" }]
};
await storage.AddItemsAsync("conv_dup_items", [item]);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => storage.AddItemsAsync("conv_dup_items", [item]));
Assert.Contains("already exists", exception.Message);
}
[Fact]
public async Task GetItemAsync_ExistingItem_ReturnsItemAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_getitem",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
var item = new ResponsesUserMessageItemResource
{
Id = "msg_getitem123",
Content = [new ItemContentInputText { Text = "Test message" }]
};
await storage.AddItemsAsync("conv_getitem", [item]);
// Act
ItemResource? result = await storage.GetItemAsync("conv_getitem", "msg_getitem123");
// Assert
Assert.NotNull(result);
Assert.Equal(item.Id, result.Id);
var userMessage = Assert.IsType<ResponsesUserMessageItemResource>(result);
Assert.NotEmpty(userMessage.Content);
}
[Fact]
public async Task GetItemAsync_NonExistentItem_ReturnsNullAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_noitem",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act
ItemResource? result = await storage.GetItemAsync("conv_noitem", "msg_nonexistent");
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetItemAsync_NonExistentConversation_ReturnsNullAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
// Act
ItemResource? result = await storage.GetItemAsync("conv_nonexistent", "msg_any");
// Assert
Assert.Null(result);
}
[Fact]
public async Task ListItemsAsync_DefaultParameters_ReturnsDescendingOrderAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_list",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Add items in order
var item1 = new ResponsesUserMessageItemResource
{
Id = "msg_001",
Content = [new ItemContentInputText { Text = "First" }]
};
var item2 = new ResponsesUserMessageItemResource
{
Id = "msg_002",
Content = [new ItemContentInputText { Text = "Second" }]
};
var item3 = new ResponsesUserMessageItemResource
{
Id = "msg_003",
Content = [new ItemContentInputText { Text = "Third" }]
};
await storage.AddItemsAsync("conv_list", [item1]);
await storage.AddItemsAsync("conv_list", [item2]);
await storage.AddItemsAsync("conv_list", [item3]);
// Act
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_list");
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Data);
Assert.Equal(3, result.Data.Count);
Assert.Equal("msg_003", result.Data[0].Id); // Descending order
Assert.Equal("msg_002", result.Data[1].Id);
Assert.Equal("msg_001", result.Data[2].Id);
Assert.Equal("msg_003", result.FirstId);
Assert.Equal("msg_001", result.LastId);
Assert.False(result.HasMore);
}
[Fact]
public async Task ListItemsAsync_AscendingOrder_ReturnsCorrectOrderAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_asc",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
var item1 = new ResponsesUserMessageItemResource
{
Id = "msg_001",
Content = [new ItemContentInputText { Text = "First" }]
};
var item2 = new ResponsesUserMessageItemResource
{
Id = "msg_002",
Content = [new ItemContentInputText { Text = "Second" }]
};
await storage.AddItemsAsync("conv_asc", [item1]);
await storage.AddItemsAsync("conv_asc", [item2]);
// Act
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_asc", order: SortOrder.Ascending);
// Assert
Assert.Equal(2, result.Data.Count);
Assert.Equal("msg_001", result.Data[0].Id); // Ascending order
Assert.Equal("msg_002", result.Data[1].Id);
}
[Fact]
public async Task ListItemsAsync_WithLimit_ReturnsCorrectPageSizeAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_limit",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
for (int i = 1; i <= 10; i++)
{
var item = new ResponsesUserMessageItemResource
{
Id = $"msg_{i:D3}",
Content = [new ItemContentInputText { Text = $"Message {i}" }]
};
await storage.AddItemsAsync("conv_limit", [item]);
}
// Act
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_limit", limit: 5);
// Assert
Assert.Equal(5, result.Data.Count);
Assert.True(result.HasMore);
Assert.Equal("msg_010", result.FirstId); // First in descending order
Assert.Equal("msg_006", result.LastId);
}
[Fact]
public async Task ListItemsAsync_WithAfter_ReturnsNextPageAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_after",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
for (int i = 1; i <= 10; i++)
{
var item = new ResponsesUserMessageItemResource
{
Id = $"msg_{i:D3}",
Content = [new ItemContentInputText { Text = $"Message {i}" }]
};
await storage.AddItemsAsync("conv_after", [item]);
}
// Act
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_after", limit: 5, after: "msg_006");
// Assert
Assert.Equal(5, result.Data.Count);
Assert.Equal("msg_005", result.Data[0].Id); // Next items after msg_006 in descending order
Assert.Equal("msg_001", result.Data[4].Id);
Assert.False(result.HasMore); // No more items after this page
}
[Fact]
public async Task ListItemsAsync_LimitClamping_ClampsToValidRangeAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_clamp",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
for (int i = 1; i <= 5; i++)
{
var item = new ResponsesUserMessageItemResource
{
Id = $"msg_{i:D3}",
Content = [new ItemContentInputText { Text = $"Message {i}" }]
};
await storage.AddItemsAsync("conv_clamp", [item]);
}
// Act - Test upper bound
ListResponse<ItemResource> result1 = await storage.ListItemsAsync("conv_clamp", limit: 200);
// Act - Test lower bound
ListResponse<ItemResource> result2 = await storage.ListItemsAsync("conv_clamp", limit: 0);
// Assert
Assert.Equal(5, result1.Data.Count); // Should return all items (clamped to 100 max, but we only have 5)
Assert.NotNull(result2.Data);
Assert.NotEmpty(result2.Data);
Assert.Single(result2.Data); // Should return at least 1 item (clamped to 1 min)
}
[Fact]
public async Task ListItemsAsync_EmptyConversation_ReturnsEmptyListAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_empty",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_empty");
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Data);
Assert.Empty(result.Data);
Assert.Null(result.FirstId);
Assert.Null(result.LastId);
Assert.False(result.HasMore);
}
[Fact]
public async Task ListItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => storage.ListItemsAsync("conv_nonexistent"));
Assert.Contains("not found", exception.Message);
}
[Fact]
public async Task DeleteItemAsync_ExistingItem_ReturnsTrueAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_delitem",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
var item = new ResponsesUserMessageItemResource
{
Id = "msg_delete",
Content = [new ItemContentInputText { Text = "Delete me" }]
};
await storage.AddItemsAsync("conv_delitem", [item]);
// Act
bool result = await storage.DeleteItemAsync("conv_delitem", "msg_delete");
// Assert
Assert.True(result);
// Verify deletion
ItemResource? retrieved = await storage.GetItemAsync("conv_delitem", "msg_delete");
Assert.Null(retrieved);
}
[Fact]
public async Task DeleteItemAsync_NonExistentItem_ReturnsFalseAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_delnoitem",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act
bool result = await storage.DeleteItemAsync("conv_delnoitem", "msg_nonexistent");
// Assert
Assert.False(result);
}
[Fact]
public async Task DeleteItemAsync_NonExistentConversation_ReturnsFalseAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
// Act
bool result = await storage.DeleteItemAsync("conv_nonexistent", "msg_any");
// Assert
Assert.False(result);
}
[Fact]
public async Task ConcurrentOperations_ThreadSafeAsync()
{
// Arrange
var storage = new InMemoryConversationStorage();
var conversation = new Conversation
{
Id = "conv_concurrent",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = []
};
await storage.CreateConversationAsync(conversation);
// Act - Add items concurrently
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
int index = i;
tasks.Add(Task.Run(async () =>
{
var item = new ResponsesUserMessageItemResource
{
Id = $"msg_{index:D3}",
Content = [new ItemContentInputText { Text = $"Message {index}" }]
};
await storage.AddItemsAsync("conv_concurrent", [item]);
}));
}
await Task.WhenAll(tasks);
// Assert
ListResponse<ItemResource> result = await storage.ListItemsAsync("conv_concurrent", limit: 100);
Assert.NotNull(result.Data);
Assert.NotEmpty(result.Data);
Assert.Equal(100, result.Data.Count);
}
}
@@ -0,0 +1,592 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Text.Json;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Tests for OpenAI Conversations 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 OpenAIConversationsSerializationTests
{
private const string TracesBasePath = "ConformanceTraces/Conversations";
/// <summary>
/// Loads a JSON file from the conformance traces directory.
/// </summary>
private static string LoadTraceFile(string relativePath)
{
var fullPath = System.IO.Path.Combine(TracesBasePath, relativePath);
if (!System.IO.File.Exists(fullPath))
{
throw new System.IO.FileNotFoundException($"Conformance trace file not found: {fullPath}");
}
return System.IO.File.ReadAllText(fullPath);
}
#region Request Serialization Tests
[Fact]
public void Deserialize_CreateConversationRequest_Success()
{
// Arrange
string json = LoadTraceFile("basic/create_conversation_request.json");
// Act
CreateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateConversationRequest);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Metadata);
}
[Fact]
public void Deserialize_CreateConversationWithItems_Success()
{
// Arrange
string json = LoadTraceFile("create_with_items/create_request.json");
// Act
CreateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateConversationRequest);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Items);
Assert.True(request.Items.Count > 0);
}
[Fact]
public void Deserialize_CreateItemsRequest_Success()
{
// Arrange
string json = LoadTraceFile("add_items/request.json");
// Act
CreateItemsRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateItemsRequest);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Items);
Assert.True(request.Items.Count > 0);
}
[Fact]
public void Deserialize_UpdateConversationRequest_Success()
{
// Arrange
string json = LoadTraceFile("update_conversation/request.json");
// Act
UpdateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.UpdateConversationRequest);
// Assert
Assert.NotNull(request);
Assert.NotNull(request.Metadata);
}
[Fact]
public void Serialize_CreateConversationRequest_MatchesFormat()
{
// Arrange
var request = new CreateConversationRequest
{
Metadata = new System.Collections.Generic.Dictionary<string, string>
{
["test_key"] = "test_value"
}
};
// Act
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
Assert.True(root.TryGetProperty("metadata", out var metadata));
Assert.Equal(JsonValueKind.Object, metadata.ValueKind);
Assert.Equal("test_value", metadata.GetProperty("test_key").GetString());
}
[Fact]
public void Serialize_CreateConversationRequestWithItems_IncludesItems()
{
// Arrange
var request = new CreateConversationRequest
{
Items =
[
new ResponsesUserMessageItemParam
{
Content = InputMessageContent.FromContents(new ItemContentInputText { Text = "test" })
}
],
Metadata = []
};
// Act
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
Assert.True(root.TryGetProperty("items", out var items));
Assert.Equal(JsonValueKind.Array, items.ValueKind);
Assert.Equal(1, items.GetArrayLength());
}
[Fact]
public void Serialize_NullableFields_AreOmittedWhenNull()
{
// Arrange
var request = new CreateConversationRequest();
// Act
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Optional fields should not be present when null or use null value
// Either the property doesn't exist or it's explicitly null
bool hasItems = root.TryGetProperty("items", out var itemsProp);
if (hasItems)
{
Assert.Equal(JsonValueKind.Null, itemsProp.ValueKind);
}
}
#endregion
#region Response Deserialization Tests
[Fact]
public void Deserialize_Conversation_Success()
{
// Arrange
string json = LoadTraceFile("basic/create_conversation_response.json");
// Act
Conversation? conversation = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Conversation);
// Assert
Assert.NotNull(conversation);
Assert.StartsWith("conv_", conversation.Id);
Assert.Equal("conversation", conversation.Object);
Assert.True(conversation.CreatedAt > 0);
Assert.NotNull(conversation.Metadata);
}
[Fact]
public void Deserialize_ConversationRoundTrip_PreservesData()
{
// Arrange
string originalJson = LoadTraceFile("basic/create_conversation_response.json");
// Act - Deserialize and re-serialize
Conversation? conversation = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.Conversation);
string reserializedJson = JsonSerializer.Serialize(conversation, OpenAIHostingJsonContext.Default.Conversation);
Conversation? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.Conversation);
// Assert
Assert.NotNull(conversation);
Assert.NotNull(roundtripped);
Assert.Equal(conversation.Id, roundtripped.Id);
Assert.Equal(conversation.CreatedAt, roundtripped.CreatedAt);
Assert.Equal(conversation.Object, roundtripped.Object);
}
[Fact]
public void Deserialize_ItemListResponse_Success()
{
// Arrange
string json = LoadTraceFile("list_items/response.json");
// Act - The list_items response uses ListResponse<ItemResource>, not ConversationListResponse
ListResponse<ItemResource>? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ListResponseItemResource);
// Assert
Assert.NotNull(response);
Assert.Equal("list", response.Object);
Assert.NotNull(response.Data);
Assert.NotNull(response.FirstId);
Assert.NotNull(response.LastId);
Assert.False(response.HasMore);
}
[Fact]
public void Deserialize_ItemResource_Success()
{
// Arrange
string json = LoadTraceFile("retrieve_item/response.json");
// Act
ItemResource? item = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ItemResource);
// Assert
Assert.NotNull(item);
Assert.StartsWith("msg_", item.Id);
Assert.Equal("message", item.Type);
var messageItem = Assert.IsType<ResponsesAssistantMessageItemResource>(item);
Assert.NotNull(messageItem.Content);
Assert.NotEmpty(messageItem.Content);
}
[Fact]
public void Deserialize_DeleteResponse_Success()
{
// Arrange
string json = LoadTraceFile("delete_conversation/response.json");
// Act
DeleteResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.DeleteResponse);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Id);
Assert.Equal("conversation.deleted", response.Object);
Assert.True(response.Deleted);
}
[Fact]
public void Deserialize_DeleteItemResponse_Success()
{
// Arrange
string json = LoadTraceFile("delete_item/response.json");
// Act
DeleteResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.DeleteResponse);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Id);
Assert.Equal("conversation.item.deleted", response.Object);
Assert.True(response.Deleted);
}
[Fact]
public void Deserialize_ErrorResponse_Success()
{
// Arrange
string json = LoadTraceFile("error_conversation_not_found/response.json");
// Act
ErrorResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ErrorResponse);
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Error);
Assert.NotNull(response.Error.Message);
Assert.NotNull(response.Error.Type);
}
[Fact]
public void Deserialize_AllConversationResponses_HaveRequiredFields()
{
// Arrange
string[] responsePaths =
[
"basic/create_conversation_response.json",
"create_with_items/create_response.json",
"retrieve_conversation/response.json",
"update_conversation/response.json"
];
foreach (var path in responsePaths)
{
string json = LoadTraceFile(path);
// Act
Conversation? conversation = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Conversation);
// Assert
Assert.NotNull(conversation);
Assert.NotNull(conversation.Id);
Assert.Equal("conversation", conversation.Object);
Assert.True(conversation.CreatedAt > 0, $"Conversation from {path} should have created_at");
}
}
[Fact]
public void Deserialize_AllItemResponses_HaveRequiredFields()
{
// Arrange - Use list_items response which has multiple items
string json = LoadTraceFile("list_items/response.json");
ListResponse<ItemResource>? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ListResponseItemResource);
Assert.NotNull(response);
Assert.NotNull(response.Data);
// Act & Assert
foreach (var item in response.Data)
{
Assert.NotNull(item);
Assert.NotNull(item.Id);
Assert.Equal("message", item.Type);
var messageItem = Assert.IsAssignableFrom<ResponsesMessageItemResource>(item);
// Content is on concrete message types (ResponsesAssistantMessageItemResource, etc.)
// For this test, we just verify the type is correct
Assert.NotNull(messageItem);
}
}
[Fact]
public void Serialize_Conversation_MatchesFormat()
{
// Arrange
var conversation = new Conversation
{
Id = "conv_test123",
CreatedAt = 1234567890,
Metadata = new System.Collections.Generic.Dictionary<string, string>
{
["test_key"] = "test_value"
}
};
// Act
string json = JsonSerializer.Serialize(conversation, OpenAIHostingJsonContext.Default.Conversation);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
Assert.Equal("conv_test123", root.GetProperty("id").GetString());
Assert.Equal("conversation", root.GetProperty("object").GetString());
Assert.Equal(1234567890, root.GetProperty("created_at").GetInt64());
var metadata = root.GetProperty("metadata");
Assert.Equal("test_value", metadata.GetProperty("test_key").GetString());
}
[Fact]
public void Serialize_ConversationListResponse_MatchesFormat()
{
// Arrange
var response = new ListResponse<Conversation>
{
Data =
[
new()
{
Id = "conv_1",
CreatedAt = 1234567890,
Metadata = []
}
],
HasMore = false
};
// Act
string json = JsonSerializer.Serialize(response, OpenAIHostingJsonUtilities.DefaultOptions);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
Assert.Equal("list", root.GetProperty("object").GetString());
var data = root.GetProperty("data");
Assert.Equal(JsonValueKind.Array, data.ValueKind);
Assert.Equal(1, data.GetArrayLength());
Assert.False(root.GetProperty("has_more").GetBoolean());
}
[Fact]
public void Serialize_DeleteResponse_MatchesFormat()
{
// Arrange
var response = new DeleteResponse
{
Id = "conv_test123",
Object = "conversation.deleted",
Deleted = true
};
// Act
string json = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.DeleteResponse);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
Assert.Equal("conv_test123", root.GetProperty("id").GetString());
Assert.Equal("conversation.deleted", root.GetProperty("object").GetString());
Assert.True(root.GetProperty("deleted").GetBoolean());
}
[Fact]
public void Serialize_ErrorResponse_MatchesFormat()
{
// Arrange
var response = new ErrorResponse
{
Error = new ErrorDetails
{
Message = "Conversation not found",
Type = "invalid_request_error"
}
};
// Act
string json = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.ErrorResponse);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert
var error = root.GetProperty("error");
Assert.Equal("Conversation not found", error.GetProperty("message").GetString());
Assert.Equal("invalid_request_error", error.GetProperty("type").GetString());
}
#endregion
#region Integration with Responses API Tests
[Fact]
public void Deserialize_ResponsesAPIRequestWithConversation_Success()
{
// Arrange
string json = LoadTraceFile("basic/first_message_request.json");
// Act
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Verify the request has conversation field
Assert.True(root.TryGetProperty("conversation", out var conversation));
var conversationId = conversation.GetString();
Assert.NotNull(conversationId);
Assert.StartsWith("conv_", conversationId);
// Assert - Has standard Responses API fields
Assert.True(root.TryGetProperty("model", out var model));
Assert.True(root.TryGetProperty("input", out var input));
Assert.True(root.TryGetProperty("max_output_tokens", out var maxTokens));
}
[Fact]
public void Deserialize_ResponsesAPIResponseWithConversation_Success()
{
// Arrange
string json = LoadTraceFile("basic/first_message_response.json");
// Act
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Verify the response has conversation field
Assert.True(root.TryGetProperty("conversation", out var conversation));
Assert.Equal(JsonValueKind.Object, conversation.ValueKind);
Assert.True(conversation.TryGetProperty("id", out var conversationId));
Assert.NotNull(conversationId.GetString());
// Assert - Has standard Responses API fields
Assert.True(root.TryGetProperty("id", out var responseId));
Assert.True(root.TryGetProperty("object", out var obj));
Assert.Equal("response", obj.GetString());
Assert.True(root.TryGetProperty("status", out var status));
Assert.True(root.TryGetProperty("output", out var output));
}
[Fact]
public void Deserialize_StreamingResponseWithConversation_Success()
{
// Arrange
string sseContent = LoadTraceFile("basic_streaming/first_message_response.txt");
// Act
var events = ParseSseEventsFromContent(sseContent);
// Assert - At least one event should be present
Assert.NotEmpty(events);
// Assert - Check if any event has conversation reference
var createdEvent = events.FirstOrDefault(e =>
e.TryGetProperty("type", out var type) &&
type.GetString() == "response.created");
if (!createdEvent.Equals(default(JsonElement)))
{
Assert.True(createdEvent.TryGetProperty("response", out var response));
// Conversation field may be in the response object
}
}
[Fact]
public void Deserialize_ImageInputWithConversation_Success()
{
// Arrange
string json = LoadTraceFile("image_input/first_message_request.json");
// Act
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Verify has conversation and image input
Assert.True(root.TryGetProperty("conversation", out var conversation));
Assert.True(root.TryGetProperty("input", out var input));
Assert.Equal(JsonValueKind.Array, input.ValueKind);
}
[Fact]
public void Deserialize_ToolCallWithConversation_Success()
{
// Arrange
string json = LoadTraceFile("tool_call/first_message_request.json");
// Act
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Verify has conversation and tools
Assert.True(root.TryGetProperty("conversation", out var conversation));
Assert.True(root.TryGetProperty("tools", out var tools));
Assert.Equal(JsonValueKind.Array, tools.ValueKind);
}
[Fact]
public void Deserialize_RefusalWithConversation_Success()
{
// Arrange
string json = LoadTraceFile("refusal/first_message_request.json");
// Act
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - Verify has conversation
Assert.True(root.TryGetProperty("conversation", out var conversation));
Assert.NotNull(conversation.GetString());
}
/// <summary>
/// Helper to parse SSE events from a streaming response content string.
/// </summary>
private static System.Collections.Generic.List<JsonElement> ParseSseEventsFromContent(string sseContent)
{
var events = new System.Collections.Generic.List<JsonElement>();
var lines = sseContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].TrimEnd('\r');
if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length)
{
var dataLine = lines[i + 1].TrimEnd('\r');
if (dataLine.StartsWith("data: ", StringComparison.Ordinal))
{
var jsonData = dataLine.Substring("data: ".Length);
var doc = JsonDocument.Parse(jsonData);
events.Add(doc.RootElement.Clone());
}
}
}
return events;
}
#endregion
}
@@ -0,0 +1,435 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
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;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Integration tests for the HTTP API with in-memory conversation, response, and agent index storage.
/// Tests create a conversation, create a response, wait for completion, then verify the conversation was updated.
/// </summary>
public sealed class OpenAIHttpApiIntegrationTests : IAsyncDisposable
{
private WebApplication? _app;
private HttpClient? _httpClient;
[Fact]
public async Task CreateConversationAndResponse_NonStreaming_NonBackground_UpdatesConversationWithOutputAsync()
{
// Arrange
const string AgentName = "test-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "The capital of France is Paris.";
const string UserMessage = "What is the capital of France?";
HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse);
// Act - Create conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = JsonSerializer.Serialize(createConversationRequest);
HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson);
using var createConvDoc = await this.ParseResponseAsync(createConvResponse);
string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!;
// Act - Create response (non-streaming, non-background)
var createResponseRequest = new
{
model = AgentName,
conversation = conversationId,
input = UserMessage,
stream = false
};
string createRespJson = JsonSerializer.Serialize(createResponseRequest);
HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson);
using var createRespDoc = await this.ParseResponseAsync(createRespResponse);
var response = createRespDoc.RootElement;
// Assert - Response completed
Assert.Equal("completed", response.GetProperty("status").GetString());
string responseId = response.GetProperty("id").GetString()!;
Assert.NotNull(responseId);
Assert.StartsWith("resp_", responseId);
// Assert - Response has output
Assert.True(response.TryGetProperty("output", out var output));
Assert.True(output.GetArrayLength() > 0);
var outputItem = output[0];
var content = outputItem.GetProperty("content");
Assert.True(content.GetArrayLength() > 0);
var textContent = content[0];
Assert.Equal("output_text", textContent.GetProperty("type").GetString());
Assert.Equal(ExpectedResponse, textContent.GetProperty("text").GetString());
// Act - List conversation items to verify they were updated
HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse);
var itemsList = listItemsDoc.RootElement;
// Assert - Conversation items were added
Assert.Equal("list", itemsList.GetProperty("object").GetString());
var items = itemsList.GetProperty("data");
Assert.True(items.GetArrayLength() > 0, "Conversation should have items after response completion");
// Find the assistant message in the items
bool foundAssistantMessage = items.EnumerateArray()
.Where(item => item.GetProperty("type").GetString() == "message" &&
item.GetProperty("role").GetString() == "assistant")
.Any(item =>
{
JsonElement itemContent = item.GetProperty("content");
if (itemContent.GetArrayLength() > 0)
{
JsonElement firstContent = itemContent[0];
return firstContent.GetProperty("type").GetString() == "output_text" &&
firstContent.GetProperty("text").GetString() == ExpectedResponse;
}
return false;
});
Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message");
}
[Fact]
public async Task CreateConversationAndResponse_Streaming_NonBackground_UpdatesConversationWithOutputAsync()
{
// Arrange
const string AgentName = "streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello there! How can I help you today?";
const string UserMessage = "Hello";
HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse);
// Act - Create conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = JsonSerializer.Serialize(createConversationRequest);
HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson);
using var createConvDoc = await this.ParseResponseAsync(createConvResponse);
string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!;
// Act - Create response (streaming, non-background)
var createResponseRequest = new
{
model = AgentName,
conversation = conversationId,
input = UserMessage,
stream = true
};
string createRespJson = JsonSerializer.Serialize(createResponseRequest);
HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson);
// Assert - Response is SSE format
Assert.Equal("text/event-stream", createRespResponse.Content.Headers.ContentType?.MediaType);
// Parse SSE events
string sseContent = await createRespResponse.Content.ReadAsStringAsync();
var events = this.ParseSseEvents(sseContent);
// Assert - Has expected event types
var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToList();
Assert.Contains("response.created", eventTypes);
Assert.Contains("response.completed", eventTypes);
// Collect the full response text from deltas
var deltaEvents = events.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
string streamedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString()));
Assert.Equal(ExpectedResponse, streamedText);
// Act - List conversation items to verify messages were added
HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse);
var itemsList = listItemsDoc.RootElement;
// Assert - Conversation items were added
var items = itemsList.GetProperty("data");
Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion");
// Find the assistant message in the items
bool foundAssistantMessage = items.EnumerateArray()
.Where(item => item.GetProperty("type").GetString() == "message" &&
item.GetProperty("role").GetString() == "assistant")
.Any(item =>
{
JsonElement itemContent = item.GetProperty("content");
if (itemContent.GetArrayLength() > 0)
{
JsonElement firstContent = itemContent[0];
return firstContent.GetProperty("type").GetString() == "output_text" &&
firstContent.GetProperty("text").GetString() == ExpectedResponse;
}
return false;
});
Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message");
}
[Fact]
public async Task CreateConversationAndResponse_NonStreaming_Background_UpdatesConversationWhenCompleteAsync()
{
// Arrange
const string AgentName = "background-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Processing in background...";
const string UserMessage = "Can you process this?";
HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse);
// Act - Create conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = JsonSerializer.Serialize(createConversationRequest);
HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson);
using var createConvDoc = await this.ParseResponseAsync(createConvResponse);
string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!;
// Act - Create response (non-streaming, background)
var createResponseRequest = new
{
model = AgentName,
conversation = conversationId,
input = UserMessage,
stream = false,
background = true
};
string createRespJson = JsonSerializer.Serialize(createResponseRequest);
HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson);
using var createRespDoc = await this.ParseResponseAsync(createRespResponse);
var response = createRespDoc.RootElement;
// Assert - Response is in progress or queued
string status = response.GetProperty("status").GetString()!;
Assert.True(status == "in_progress" || status == "queued" || status == "completed", $"Expected 'in_progress', 'queued', or 'completed', got '{status}'");
string responseId = response.GetProperty("id").GetString()!;
// Wait for completion by polling
const int MaxAttempts = 20;
int attempt = 0;
string finalStatus = status;
string? errorMessage = null;
while (finalStatus != "completed" && finalStatus != "failed" && attempt < MaxAttempts)
{
await Task.Delay(100);
HttpResponseMessage getResponseResponse = await this.SendGetRequestAsync(client, $"/{AgentName}/v1/responses/{responseId}");
using var getRespDoc = await this.ParseResponseAsync(getResponseResponse);
finalStatus = getRespDoc.RootElement.GetProperty("status").GetString()!;
if (getRespDoc.RootElement.TryGetProperty("error", out var error) &&
error.ValueKind == JsonValueKind.Object &&
error.TryGetProperty("message", out var messageElement))
{
errorMessage = messageElement.GetString();
}
attempt++;
}
// Assert - Response eventually completed
Assert.Equal("completed", finalStatus + (errorMessage != null ? $" Error: {errorMessage}" : ""));
// Act - List conversation items to verify messages were added
HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse);
var itemsList = listItemsDoc.RootElement;
// Assert - Conversation items were added
var items = itemsList.GetProperty("data");
Assert.True(items.GetArrayLength() > 0, "Conversation should have items after background response completion");
// Find the assistant message in the items
bool foundAssistantMessage = items.EnumerateArray()
.Where(item => item.GetProperty("type").GetString() == "message" &&
item.GetProperty("role").GetString() == "assistant")
.Any(item =>
{
JsonElement itemContent = item.GetProperty("content");
if (itemContent.GetArrayLength() > 0)
{
JsonElement firstContent = itemContent[0];
return firstContent.GetProperty("type").GetString() == "output_text" &&
firstContent.GetProperty("text").GetString() == ExpectedResponse;
}
return false;
});
Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message");
}
[Fact]
public async Task CreateConversationAndResponse_Streaming_Background_UpdatesConversationWhenCompleteAsync()
{
// Arrange
const string AgentName = "streaming-background-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Streaming background response";
const string UserMessage = "Process this with streaming";
HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse);
// Act - Create conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = JsonSerializer.Serialize(createConversationRequest);
HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson);
using var createConvDoc = await this.ParseResponseAsync(createConvResponse);
string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!;
// Act - Create response (streaming, background)
var createResponseRequest = new
{
model = AgentName,
conversation = conversationId,
input = UserMessage,
stream = true,
background = false // Note: streaming with background=true is typically streaming
};
string createRespJson = JsonSerializer.Serialize(createResponseRequest);
HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson);
// Assert - Response is SSE format
Assert.Equal("text/event-stream", createRespResponse.Content.Headers.ContentType?.MediaType);
// Parse SSE events
string sseContent = await createRespResponse.Content.ReadAsStringAsync();
var events = this.ParseSseEvents(sseContent);
var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToList();
Assert.Contains("response.created", eventTypes);
Assert.Contains("response.completed", eventTypes);
// Act - List conversation items to verify messages were added
HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse);
var itemsList = listItemsDoc.RootElement;
// Assert - Conversation items were added
var items = itemsList.GetProperty("data");
Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion");
// Find the assistant message in the items
bool foundAssistantMessage = items.EnumerateArray()
.Where(item => item.GetProperty("type").GetString() == "message" &&
item.GetProperty("role").GetString() == "assistant")
.Any(item =>
{
JsonElement itemContent = item.GetProperty("content");
if (itemContent.GetArrayLength() > 0)
{
JsonElement firstContent = itemContent[0];
return firstContent.GetProperty("type").GetString() == "output_text";
}
return false;
});
Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message");
}
/// <summary>
/// Creates a test server with in-memory conversation, response, and agent index storage.
/// </summary>
private async Task<HttpClient> CreateTestServerWithInMemoryStorageAsync(string agentName, string instructions, string responseText)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
// Create mock chat client
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
// Add agent
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
// Add in-memory storage for conversations, responses, and agent index
builder.AddOpenAIConversations();
builder.AddOpenAIResponses();
this._app = builder.Build();
// Map endpoints
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIConversations();
this._app.MapOpenAIResponses(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
this._httpClient = testServer.CreateClient();
return this._httpClient;
}
/// <summary>
/// Sends a POST request with JSON content to the test server.
/// </summary>
private async Task<HttpResponseMessage> SendPostRequestAsync(HttpClient client, string path, string requestJson)
{
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
return await client.PostAsync(new Uri(path, UriKind.Relative), content);
}
/// <summary>
/// Sends a GET request to the test server.
/// </summary>
private async Task<HttpResponseMessage> SendGetRequestAsync(HttpClient client, string path)
{
return await client.GetAsync(new Uri(path, UriKind.Relative));
}
/// <summary>
/// Parses the response JSON and returns a JsonDocument.
/// </summary>
private async Task<JsonDocument> ParseResponseAsync(HttpResponseMessage response)
{
string responseJson = await response.Content.ReadAsStringAsync();
return JsonDocument.Parse(responseJson);
}
/// <summary>
/// Parses SSE events from streaming response content string.
/// </summary>
private JsonElement[] ParseSseEvents(string sseContent)
{
var events = new System.Collections.Generic.List<JsonElement>();
var lines = sseContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].TrimEnd('\r');
if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length)
{
var dataLine = lines[i + 1].TrimEnd('\r');
if (dataLine.StartsWith("data: ", StringComparison.Ordinal))
{
var jsonData = dataLine.Substring("data: ".Length);
if (!string.IsNullOrWhiteSpace(jsonData))
{
var doc = JsonDocument.Parse(jsonData);
events.Add(doc.RootElement.Clone());
}
}
}
}
return events.ToArray();
}
public async ValueTask DisposeAsync()
{
this._httpClient?.Dispose();
if (this._app != null)
{
await this._app.DisposeAsync();
}
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,439 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
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;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Integration tests for the MapOpenAIResponses variant that resolves agents from the Agent.Name property.
/// These tests validate the agent resolution mechanism using the HostedAgentResponseExecutor.
/// </summary>
public sealed class OpenAIResponsesAgentResolutionIntegrationTests : 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 agent resolution works using the agent.name property in streaming mode.
/// </summary>
[Fact]
public async Task CreateResponseStreaming_WithAgentNameProperty_ResolvesCorrectAgentAsync()
{
// Arrange
const string AgentName = "test-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello from agent resolution!";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(AgentName, Instructions, ExpectedResponse));
// Act - Use raw HTTP request with agent.name specified
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
agent = new { name = AgentName },
stream = true,
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}");
string responseText = await httpResponse.Content.ReadAsStringAsync();
Assert.Contains(ExpectedResponse, responseText);
Assert.Contains("response.created", responseText);
Assert.Contains("response.completed", responseText);
}
/// <summary>
/// Verifies that agent resolution works using the agent.name property in non-streaming mode.
/// </summary>
[Fact]
public async Task CreateResponse_WithAgentNameProperty_ResolvesCorrectAgentAsync()
{
// Arrange
const string AgentName = "test-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello from agent resolution!";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(AgentName, Instructions, ExpectedResponse));
// Act - Use raw HTTP request with agent.name specified
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
agent = new { name = AgentName },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}");
string responseJson = await httpResponse.Content.ReadAsStringAsync();
using JsonDocument doc = JsonDocument.Parse(responseJson);
JsonElement root = doc.RootElement;
Assert.Equal("completed", root.GetProperty("status").GetString());
JsonElement outputArray = root.GetProperty("output");
Assert.True(outputArray.GetArrayLength() > 0);
JsonElement firstOutput = outputArray[0];
JsonElement contentArray = firstOutput.GetProperty("content");
JsonElement firstContent = contentArray[0];
string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty;
Assert.Equal(ExpectedResponse, actualResponse);
}
/// <summary>
/// Verifies that agent resolution can distinguish between multiple agents.
/// </summary>
[Fact]
public async Task CreateResponse_WithMultipleAgents_ResolvesCorrectAgentAsync()
{
// Arrange
const string Agent1Name = "agent-1";
const string Agent1Response = "Response from agent 1";
const string Agent2Name = "agent-2";
const string Agent2Response = "Response from agent 2";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(Agent1Name, "Agent 1 instructions", Agent1Response),
(Agent2Name, "Agent 2 instructions", Agent2Response));
// Act - Create response for agent 1
using StringContent requestContent1 = new(JsonSerializer.Serialize(new
{
agent = new { name = Agent1Name },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse1 = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent1);
// Act - Create response for agent 2
using StringContent requestContent2 = new(JsonSerializer.Serialize(new
{
agent = new { name = Agent2Name },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse2 = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent2);
// Assert
string responseJson1 = await httpResponse1.Content.ReadAsStringAsync();
string responseJson2 = await httpResponse2.Content.ReadAsStringAsync();
using JsonDocument doc1 = JsonDocument.Parse(responseJson1);
using JsonDocument doc2 = JsonDocument.Parse(responseJson2);
string content1 = doc1.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty;
string content2 = doc2.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty;
Assert.Equal(Agent1Response, content1);
Assert.Equal(Agent2Response, content2);
}
/// <summary>
/// Verifies that agent resolution using the model property works correctly.
/// </summary>
[Fact]
public async Task CreateResponse_WithModelProperty_ResolvesCorrectAgentAsync()
{
// Arrange
const string AgentName = "model-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response via model property";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(AgentName, Instructions, ExpectedResponse));
// Act - Use raw HTTP request to control the model property
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
model = AgentName,
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}");
string responseJson = await httpResponse.Content.ReadAsStringAsync();
using JsonDocument doc = JsonDocument.Parse(responseJson);
JsonElement root = doc.RootElement;
Assert.Equal("completed", root.GetProperty("status").GetString());
JsonElement outputArray = root.GetProperty("output");
Assert.True(outputArray.GetArrayLength() > 0);
JsonElement firstOutput = outputArray[0];
JsonElement contentArray = firstOutput.GetProperty("content");
JsonElement firstContent = contentArray[0];
string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty;
Assert.Equal(ExpectedResponse, actualResponse);
}
/// <summary>
/// Verifies that agent resolution fails gracefully when agent is not found.
/// </summary>
[Fact]
public async Task CreateResponse_WithNonExistentAgent_ReturnsNotFoundAsync()
{
// Arrange
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
("existing-agent", "Instructions", "Response"));
// Act
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
agent = new { name = "non-existent-agent" },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.Equal(System.Net.HttpStatusCode.NotFound, httpResponse.StatusCode);
string responseJson = await httpResponse.Content.ReadAsStringAsync();
Assert.Contains("non-existent-agent", responseJson);
Assert.Contains("not found", responseJson, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Verifies that agent resolution fails gracefully when no agent name is provided.
/// </summary>
[Fact]
public async Task CreateResponse_WithoutAgentOrModel_ReturnsBadRequestAsync()
{
// Arrange
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
("test-agent", "Instructions", "Response"));
// Act - Use raw HTTP request without agent.name or model
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode);
string responseJson = await httpResponse.Content.ReadAsStringAsync();
Assert.Contains("agent.name", responseJson, StringComparison.OrdinalIgnoreCase);
Assert.Contains("model", responseJson, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Verifies that agent resolution prioritizes agent.name over model when both are provided.
/// </summary>
[Fact]
public async Task CreateResponse_WithBothAgentAndModel_UsesAgentNameAsync()
{
// Arrange
const string Agent1Name = "agent-1";
const string Agent1Response = "Response from agent 1";
const string Agent2Name = "agent-2";
const string Agent2Response = "Response from agent 2";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(Agent1Name, "Agent 1 instructions", Agent1Response),
(Agent2Name, "Agent 2 instructions", Agent2Response));
// Act - Use raw HTTP request with both agent.name and model
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
agent = new { name = Agent1Name },
model = Agent2Name,
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.True(httpResponse.IsSuccessStatusCode);
string responseJson = await httpResponse.Content.ReadAsStringAsync();
using JsonDocument doc = JsonDocument.Parse(responseJson);
JsonElement root = doc.RootElement;
JsonElement outputArray = root.GetProperty("output");
JsonElement firstOutput = outputArray[0];
JsonElement contentArray = firstOutput.GetProperty("content");
JsonElement firstContent = contentArray[0];
string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty;
// Should use agent.name (Agent1Name) and return Agent1Response
Assert.Equal(Agent1Response, actualResponse);
}
/// <summary>
/// Verifies that streaming and non-streaming work correctly with agent resolution.
/// </summary>
[Fact]
public async Task CreateResponse_AgentResolution_StreamingAndNonStreamingBothWorkAsync()
{
// 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.CreateTestServerWithAgentResolutionAsync(
(AgentName, Instructions, ExpectedResponse));
// Act - Non-streaming
using StringContent nonStreamingRequest = new(JsonSerializer.Serialize(new
{
agent = new { name = AgentName },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage nonStreamingHttpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), nonStreamingRequest);
// Act - Streaming
using StringContent streamingRequest = new(JsonSerializer.Serialize(new
{
agent = new { name = AgentName },
stream = true,
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage streamingHttpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), streamingRequest);
// Assert non-streaming
string nonStreamingJson = await nonStreamingHttpResponse.Content.ReadAsStringAsync();
using JsonDocument nonStreamingDoc = JsonDocument.Parse(nonStreamingJson);
string nonStreamingContent = nonStreamingDoc.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty;
// Assert streaming
string streamingText = await streamingHttpResponse.Content.ReadAsStringAsync();
Assert.Equal(ExpectedResponse, nonStreamingContent);
Assert.Contains(ExpectedResponse, streamingText);
}
/// <summary>
/// Verifies that the agent.name field is populated in the response.
/// </summary>
[Fact]
public async Task CreateResponse_WithAgentName_ResponseIncludesAgentFieldAsync()
{
// Arrange
const string AgentName = "test-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello";
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
(AgentName, Instructions, ExpectedResponse));
// Act
using StringContent requestContent = new(JsonSerializer.Serialize(new
{
agent = new { name = AgentName },
input = new[]
{
new { type = "message", role = "user", content = "Test message" }
}
}), Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent);
// Assert
Assert.True(httpResponse.IsSuccessStatusCode);
string responseJson = await httpResponse.Content.ReadAsStringAsync();
using JsonDocument doc = JsonDocument.Parse(responseJson);
JsonElement root = doc.RootElement;
// Verify the response includes the agent field
if (root.TryGetProperty("agent", out JsonElement agentElement))
{
string? agentNameInResponse = agentElement.GetProperty("name").GetString();
Assert.Equal(AgentName, agentNameInResponse);
}
}
private async Task<HttpClient> CreateTestServerWithAgentResolutionAsync(
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.AddOpenAIResponses();
this._app = builder.Build();
// Use the agent resolution variant - MapOpenAIResponses() without agent parameter
this._app.MapOpenAIResponses();
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
}
@@ -14,7 +14,6 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// Conformance tests for OpenAI Responses 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.
/// For pure serialization/deserialization tests, see OpenAIResponsesSerializationTests.
/// </summary>
public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
{
@@ -38,18 +37,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
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, "input");
AssertJsonPropertyEquals(request, "max_output_tokens", 100);
var input = request.GetProperty("input");
Assert.Equal(JsonValueKind.String, input.ValueKind);
Assert.Equal("Hello, how are you?", input.GetString());
// Assert - Response metadata (IDs and timestamps are dynamic, just verify structure)
AssertJsonPropertyExists(response, "id");
AssertJsonPropertyEquals(response, "object", "response");
@@ -147,11 +134,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
Assert.Equal(JsonValueKind.Null, response.GetProperty("previous_response_id").ValueKind);
// Assert - Service tier and store
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}'");
AssertJsonPropertyExists(response, "store");
Assert.Equal(JsonValueKind.True, response.GetProperty("store").ValueKind);
}
@@ -169,6 +151,15 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
.GetProperty("content")[0]
.GetProperty("text").GetString()!;
// Parse the request to verify it has previous_response_id
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
var previousResponseId = request.GetProperty("previous_response_id").GetString();
Assert.NotNull(previousResponseId);
Assert.NotEmpty(previousResponseId);
// Use stateful mock that tracks conversation state by returning different responses
// First call (initial message) vs second call (conversation continuation)
HttpClient client = await this.CreateTestServerAsync("conversation-agent", "You are a helpful assistant.", expectedText);
// Act
@@ -176,36 +167,20 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
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 previous_response_id (structure verification)
AssertJsonPropertyExists(request, "previous_response_id");
var previousResponseId = request.GetProperty("previous_response_id").GetString();
Assert.NotNull(previousResponseId);
Assert.StartsWith("resp_", previousResponseId);
Assert.NotEmpty(previousResponseId);
// Assert - Request structure
AssertJsonPropertyEquals(request, "model", "gpt-4o-mini");
AssertJsonPropertyExists(request, "input");
AssertJsonPropertyExists(request, "previous_response_id");
AssertJsonPropertyExists(request, "max_output_tokens");
var input = request.GetProperty("input");
Assert.Equal(JsonValueKind.String, input.ValueKind);
// Assert - Response should have previous_response_id field preserved from request
AssertJsonPropertyExists(response, "previous_response_id");
var responsePreviousId = response.GetProperty("previous_response_id").GetString();
Assert.Equal(previousResponseId, responsePreviousId);
// Assert - Response has unique ID
// Assert - Response has unique ID (must be different from previous_response_id)
var currentId = response.GetProperty("id").GetString();
Assert.NotNull(currentId);
Assert.StartsWith("resp_", currentId);
Assert.NotEqual(previousResponseId, currentId);
// Assert - Usage includes context from previous response
// The system should pass accumulated conversation history to the chat client,
// resulting in higher input token counts than a single-message request
AssertJsonPropertyExists(response, "usage");
var usage = response.GetProperty("usage");
var inputTokens = usage.GetProperty("input_tokens").GetInt32();
@@ -279,112 +254,62 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string functionName = functionCall.GetProperty("name").GetString()!;
string arguments = functionCall.GetProperty("arguments").GetString()!;
HttpClient client = await this.CreateTestServerAsync("tool-agent", "You are a helpful assistant.", functionName);
// Use tool call mock that returns FunctionCallContent from the chat client
// This simulates the chat client (e.g., OpenAI) deciding to call a function
// The test validates that our system correctly processes and serializes
// the function call into the OpenAI Responses API format
HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-agent", "You are a helpful assistant.", functionName, arguments);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "tool-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 requestTools = request.GetProperty("tools");
Assert.Equal(JsonValueKind.Array, requestTools.ValueKind);
Assert.True(requestTools.GetArrayLength() > 0, "Tools array should not be empty");
// Assert - Tool has correct structure
var requestTool = requestTools[0];
AssertJsonPropertyEquals(requestTool, "type", "function");
AssertJsonPropertyExists(requestTool, "name");
AssertJsonPropertyExists(requestTool, "description");
AssertJsonPropertyExists(requestTool, "parameters");
var requestToolName = requestTool.GetProperty("name").GetString();
Assert.Equal("get_weather", requestToolName);
// Assert - Parameters have JSON Schema
var requestParameters = requestTool.GetProperty("parameters");
AssertJsonPropertyEquals(requestParameters, "type", "object");
AssertJsonPropertyExists(requestParameters, "properties");
AssertJsonPropertyExists(requestParameters, "required");
var requestProperties = requestParameters.GetProperty("properties");
Assert.Equal(JsonValueKind.Object, requestProperties.ValueKind);
AssertJsonPropertyExists(requestProperties, "location");
AssertJsonPropertyExists(requestProperties, "unit");
// Assert - Property has type and description
var locationProperty = requestProperties.GetProperty("location");
AssertJsonPropertyEquals(locationProperty, "type", "string");
AssertJsonPropertyExists(locationProperty, "description");
var description = locationProperty.GetProperty("description").GetString();
Assert.NotNull(description);
Assert.NotEmpty(description);
// Assert - Required fields is array
var requestRequired = requestParameters.GetProperty("required");
Assert.Equal(JsonValueKind.Array, requestRequired.ValueKind);
var requestRequiredFields = requestRequired.EnumerateArray().Select(e => e.GetString()).ToList();
Assert.Contains("location", requestRequiredFields);
// Assert - Request has tool choice
AssertJsonPropertyExists(request, "tool_choice");
var toolChoice = request.GetProperty("tool_choice").GetString();
Assert.Equal("auto", toolChoice);
// Assert - Response has function call output (or text output depending on implementation)
// Assert - Response has function call output
AssertJsonPropertyExists(response, "output");
var output = response.GetProperty("output");
Assert.Equal(JsonValueKind.Array, output.ValueKind);
Assert.True(output.GetArrayLength() > 0);
var responseItem = output[0];
// Our implementation may return either function_call or message type
// Assert - Response item type is function_call (system properly converted FunctionCallContent)
var itemType = responseItem.GetProperty("type").GetString();
if (itemType == "function_call")
{
AssertJsonPropertyEquals(responseItem, "type", "function_call");
AssertJsonPropertyEquals(responseItem, "type", "function_call");
// Assert - Function call has name
AssertJsonPropertyExists(responseItem, "name");
var funcName = responseItem.GetProperty("name").GetString();
Assert.Equal("get_weather", funcName);
// Assert - Function call has correct name (from chat client)
AssertJsonPropertyExists(responseItem, "name");
var funcName = responseItem.GetProperty("name").GetString();
Assert.Equal("get_weather", funcName);
// Assert - Function call has arguments
AssertJsonPropertyExists(responseItem, "arguments");
var argsString = responseItem.GetProperty("arguments").GetString();
Assert.NotNull(argsString);
Assert.NotEmpty(argsString);
var argsDoc = JsonDocument.Parse(argsString);
var argsRoot = argsDoc.RootElement;
AssertJsonPropertyExists(argsRoot, "location");
var location = argsRoot.GetProperty("location").GetString();
Assert.Contains("San Francisco", location);
}
// Assert - Function call has arguments (properly serialized from chat client response)
AssertJsonPropertyExists(responseItem, "arguments");
var argsString = responseItem.GetProperty("arguments").GetString();
Assert.NotNull(argsString);
Assert.NotEmpty(argsString);
var argsDoc = JsonDocument.Parse(argsString);
var argsRoot = argsDoc.RootElement;
AssertJsonPropertyExists(argsRoot, "location");
var location = argsRoot.GetProperty("location").GetString();
Assert.Contains("San Francisco", location);
if (itemType == "function_call")
{
// Assert - Function call has call_id and id
AssertJsonPropertyExists(responseItem, "call_id");
var callId = responseItem.GetProperty("call_id").GetString();
Assert.NotNull(callId);
Assert.NotEmpty(callId);
Assert.StartsWith("call_", callId);
AssertJsonPropertyExists(responseItem, "id");
var itemId = responseItem.GetProperty("id").GetString();
Assert.NotNull(itemId);
Assert.NotEmpty(itemId);
Assert.StartsWith("fc_", itemId);
// Assert - Function call has call_id and id (system generates these)
AssertJsonPropertyExists(responseItem, "call_id");
var callId = responseItem.GetProperty("call_id").GetString();
Assert.NotNull(callId);
Assert.NotEmpty(callId);
Assert.StartsWith("call_", callId);
AssertJsonPropertyExists(responseItem, "id");
var itemId = responseItem.GetProperty("id").GetString();
Assert.NotNull(itemId);
Assert.NotEmpty(itemId);
Assert.StartsWith("func_", itemId);
// Assert - Function call has status
AssertJsonPropertyExists(responseItem, "status");
var itemStatus = responseItem.GetProperty("status").GetString();
Assert.Equal("completed", itemStatus);
}
// Assert - Function call has status
AssertJsonPropertyExists(responseItem, "status");
var itemStatus = responseItem.GetProperty("status").GetString();
Assert.Equal("completed", itemStatus);
// Assert - Response preserves tool definitions
// Assert - Response preserves tool definitions from request
var responseTools = response.GetProperty("tools");
Assert.Equal(JsonValueKind.Array, responseTools.ValueKind);
Assert.True(responseTools.GetArrayLength() > 0);
@@ -394,7 +319,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
AssertJsonPropertyExists(responseTool, "description");
AssertJsonPropertyExists(responseTool, "parameters");
// Assert - Response has usage statistics
// Assert - Response has usage statistics (includes tool definition overhead)
AssertJsonPropertyExists(response, "usage");
var usage = response.GetProperty("usage");
var inputTokens = usage.GetProperty("input_tokens").GetInt32();
@@ -448,13 +373,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEventsFromContent(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 is valid SSE format
var lines = responseSse.Split('\n');
Assert.NotEmpty(lines);
@@ -504,7 +422,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
Assert.Equal("response.created", eventTypes[0]);
Assert.Equal("response.in_progress", eventTypes[1]);
var lastEvent = eventTypes[^1];
Assert.True(lastEvent == "response.completed" || lastEvent == "response.incomplete",
Assert.True(lastEvent is "response.completed" or "response.incomplete",
$"Last event should be terminal state, got: {lastEvent}");
// Assert - Created event has response object
@@ -577,13 +495,13 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
var finalEvent = events.FirstOrDefault(e =>
{
var type = e.GetProperty("type").GetString();
return type == "response.completed" || type == "response.incomplete";
return type is "response.completed" or "response.incomplete";
});
Assert.False(finalEvent.Equals(default(JsonElement)), "Should have a terminal response event");
AssertJsonPropertyExists(finalEvent, "response");
var finalResponse = finalEvent.GetProperty("response");
var finalStatus = finalResponse.GetProperty("status").GetString();
Assert.True(finalStatus == "completed" || finalStatus == "incomplete",
Assert.True(finalStatus is "completed" or "incomplete",
$"Status should be completed or incomplete, got: {finalStatus}");
AssertJsonPropertyExists(finalResponse, "output");
var finalOutput = finalResponse.GetProperty("output");
@@ -650,39 +568,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
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 metadata object
AssertJsonPropertyExists(request, "metadata");
var requestMetadata = request.GetProperty("metadata");
Assert.Equal(JsonValueKind.Object, requestMetadata.ValueKind);
// Assert - Request has custom metadata fields
AssertJsonPropertyEquals(requestMetadata, "user_id", "test_user_123");
AssertJsonPropertyEquals(requestMetadata, "session_id", "session_456");
AssertJsonPropertyEquals(requestMetadata, "purpose", "conformance_test");
// Assert - Request has instructions
AssertJsonPropertyExists(request, "instructions");
var requestInstructions = request.GetProperty("instructions").GetString();
Assert.NotNull(requestInstructions);
Assert.NotEmpty(requestInstructions);
Assert.Equal("Respond in a friendly, educational tone.", requestInstructions);
// Assert - Request has temperature parameter
AssertJsonPropertyExists(request, "temperature");
var requestTemperature = request.GetProperty("temperature").GetDouble();
Assert.Equal(0.7, requestTemperature);
Assert.InRange(requestTemperature, 0.0, 2.0);
// Assert - Request has top_p parameter
AssertJsonPropertyExists(request, "top_p");
var requestTopP = request.GetProperty("top_p").GetDouble();
Assert.Equal(0.9, requestTopP);
Assert.InRange(requestTopP, 0.0, 1.0);
// Assert - Response preserves metadata
var responseMetadata = response.GetProperty("metadata");
AssertJsonPropertyEquals(responseMetadata, "user_id", "test_user_123");
@@ -690,22 +575,21 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
AssertJsonPropertyEquals(responseMetadata, "purpose", "conformance_test");
// Assert - Response preserves instructions
var responseInstructions = response.GetProperty("instructions").GetString();
Assert.Equal(requestInstructions, responseInstructions);
AssertJsonPropertyEquals(response, "instructions", "Respond in a friendly, educational tone.");
// Assert - Response preserves temperature
var responseTemperature = response.GetProperty("temperature").GetDouble();
Assert.Equal(requestTemperature, responseTemperature);
Assert.Equal(0.7, responseTemperature);
// Assert - Response preserves top_p
var responseTopP = response.GetProperty("top_p").GetDouble();
Assert.Equal(requestTopP, responseTopP);
Assert.Equal(0.9, responseTopP);
// Assert - Response status (may be incomplete if max_output_tokens was respected)
AssertJsonPropertyExists(response, "status");
var status = response.GetProperty("status").GetString();
// Our implementation may complete even with max_output_tokens if response fits
Assert.True(status == "completed" || status == "incomplete");
Assert.True(status is "completed" or "incomplete");
// Assert - Response has incomplete_details field
AssertJsonPropertyExists(response, "incomplete_details");
@@ -770,25 +654,28 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
.GetProperty("content")[0]
.GetProperty("text").GetString()!;
HttpClient client = await this.CreateTestServerAsync("reasoning-agent", "You are a helpful assistant.", expectedText);
// Get expected reasoning summary text (if any)
var reasoningSummary = expectedResponse.GetProperty("output")[0].GetProperty("summary");
string reasoningText = reasoningSummary.GetArrayLength() > 0
? reasoningSummary[0].GetProperty("text").GetString()!
: "Thinking about the problem...";
// Create a custom content provider that returns reasoning content followed by regular text
HttpClient client = await this.CreateTestServerAsync(
"reasoning-agent",
"You are a helpful assistant.",
expectedText,
contentProvider: _ =>
[
new Extensions.AI.TextReasoningContent(reasoningText),
new Extensions.AI.TextContent(expectedText)
]);
// Act
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "reasoning-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 reasoning configuration
AssertJsonPropertyExists(request, "reasoning");
var requestReasoning = request.GetProperty("reasoning");
Assert.Equal(JsonValueKind.Object, requestReasoning.ValueKind);
AssertJsonPropertyExists(requestReasoning, "effort");
var effort = requestReasoning.GetProperty("effort").GetString();
Assert.Equal("medium", effort);
// Assert - Response preserves reasoning configuration
AssertJsonPropertyExists(response, "reasoning");
var responseReasoning = response.GetProperty("reasoning");
@@ -859,30 +746,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
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 text format with json_schema
AssertJsonPropertyExists(request, "text");
var requestText = request.GetProperty("text");
AssertJsonPropertyExists(requestText, "format");
var format = requestText.GetProperty("format");
AssertJsonPropertyEquals(format, "type", "json_schema");
AssertJsonPropertyEquals(format, "name", "person");
AssertJsonPropertyEquals(format, "strict", true);
// Assert - Schema has correct structure
AssertJsonPropertyExists(format, "schema");
var schema = format.GetProperty("schema");
AssertJsonPropertyEquals(schema, "type", "object");
AssertJsonPropertyExists(schema, "properties");
AssertJsonPropertyExists(schema, "required");
var properties = schema.GetProperty("properties");
AssertJsonPropertyExists(properties, "name");
AssertJsonPropertyExists(properties, "age");
AssertJsonPropertyExists(properties, "occupation");
// Assert - Response preserves text format configuration
AssertJsonPropertyExists(response, "text");
var responseText = response.GetProperty("text");
@@ -903,6 +766,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
Assert.Equal(expectedText, text);
// Assert - Output text is valid JSON matching schema
// This validates that the mock/system produced well-formed JSON output
using var jsonDoc = JsonDocument.Parse(text);
var jsonRoot = jsonDoc.RootElement;
AssertJsonPropertyExists(jsonRoot, "name");
@@ -1002,38 +866,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
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 input array with message
AssertJsonPropertyExists(request, "input");
var input = request.GetProperty("input");
Assert.Equal(JsonValueKind.Array, input.ValueKind);
Assert.True(input.GetArrayLength() > 0);
// Assert - Input message has content with image
var inputMessage = input[0];
AssertJsonPropertyEquals(inputMessage, "type", "message");
AssertJsonPropertyEquals(inputMessage, "role", "user");
AssertJsonPropertyExists(inputMessage, "content");
var inputContent = inputMessage.GetProperty("content");
Assert.Equal(JsonValueKind.Array, inputContent.ValueKind);
Assert.True(inputContent.GetArrayLength() >= 2, "Content should have text and image");
// Assert - Content has input_text
var textPart = inputContent[0];
AssertJsonPropertyEquals(textPart, "type", "input_text");
AssertJsonPropertyExists(textPart, "text");
// Assert - Content has input_image
var imagePart = inputContent[1];
AssertJsonPropertyEquals(imagePart, "type", "input_image");
AssertJsonPropertyExists(imagePart, "image_url");
var imageUrl = imagePart.GetProperty("image_url").GetString();
Assert.NotNull(imageUrl);
Assert.NotEmpty(imageUrl);
// Assert - Response has output
AssertJsonPropertyExists(response, "output");
var output = response.GetProperty("output");
@@ -1078,16 +910,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEventsFromContent(responseSse);
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has stream flag and reasoning configuration
AssertJsonPropertyEquals(request, "stream", true);
AssertJsonPropertyExists(request, "reasoning");
var reasoning = request.GetProperty("reasoning");
AssertJsonPropertyExists(reasoning, "effort");
// Assert - Response has event types for reasoning
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
Assert.Contains("response.created", eventTypes);
@@ -1120,7 +942,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
var finalEvent = events.FirstOrDefault(e =>
{
var type = e.GetProperty("type").GetString();
return type == "response.completed" || type == "response.incomplete";
return type is "response.completed" or "response.incomplete";
});
Assert.False(finalEvent.Equals(default(JsonElement)));
var finalResponse = finalEvent.GetProperty("response");
@@ -1156,17 +978,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEventsFromContent(responseSse);
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has stream flag and json_schema format
AssertJsonPropertyEquals(request, "stream", true);
AssertJsonPropertyExists(request, "text");
var text = request.GetProperty("text");
var format = text.GetProperty("format");
AssertJsonPropertyEquals(format, "type", "json_schema");
// Assert - Response has standard streaming events
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
Assert.Contains("response.created", eventTypes);
@@ -1176,7 +987,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
var finalEvent = events.FirstOrDefault(e =>
{
var type = e.GetProperty("type").GetString();
return type == "response.completed" || type == "response.incomplete";
return type is "response.completed" or "response.incomplete";
});
Assert.False(finalEvent.Equals(default(JsonElement)));
var finalResponse = finalEvent.GetProperty("response");
@@ -1216,13 +1027,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEventsFromContent(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 standard streaming events
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
Assert.Contains("response.created", eventTypes);
@@ -1232,12 +1036,12 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
var finalEvent = events.FirstOrDefault(e =>
{
var type = e.GetProperty("type").GetString();
return type == "response.completed" || type == "response.incomplete";
return type is "response.completed" or "response.incomplete";
});
Assert.False(finalEvent.Equals(default(JsonElement)));
var finalResponse = finalEvent.GetProperty("response");
var status = finalResponse.GetProperty("status").GetString();
Assert.True(status == "completed" || status == "incomplete");
Assert.True(status is "completed" or "incomplete");
// Assert - Text done has refusal content
var doneEvent = events.First(e => e.GetProperty("type").GetString() == "response.output_text.done");
@@ -1273,30 +1077,6 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
string responseSse = await httpResponse.Content.ReadAsStringAsync();
var events = ParseSseEventsFromContent(responseSse);
// Parse the request
using var requestDoc = JsonDocument.Parse(requestJson);
var request = requestDoc.RootElement;
// Assert - Request has stream flag
AssertJsonPropertyEquals(request, "stream", true);
// Assert - Request has input array with image
AssertJsonPropertyExists(request, "input");
var input = request.GetProperty("input");
Assert.Equal(JsonValueKind.Array, input.ValueKind);
var inputMessage = input[0];
var inputContent = inputMessage.GetProperty("content");
bool hasImage = false;
foreach (var part in inputContent.EnumerateArray())
{
if (part.GetProperty("type").GetString() == "input_image")
{
hasImage = true;
break;
}
}
Assert.True(hasImage, "Request should have input_image content");
// Assert - Response has standard streaming events
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
Assert.Contains("response.created", eventTypes);
@@ -1306,7 +1086,7 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
var finalEvent = events.FirstOrDefault(e =>
{
var type = e.GetProperty("type").GetString();
return type == "response.completed" || type == "response.incomplete";
return type is "response.completed" or "response.incomplete";
});
Assert.False(finalEvent.Equals(default(JsonElement)));
var finalResponse = finalEvent.GetProperty("response");
@@ -1319,9 +1099,35 @@ public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase
Assert.NotEmpty(finalText);
}
/// <summary>
/// Helper to parse SSE events from a streaming response content string.
/// </summary>
[Fact]
public async Task MutualExclusiveErrorAsync()
{
// Arrange
string requestJson = LoadResponsesTraceFile("mutual_exclusive_error/request.json");
using var expectedResponseDoc = LoadResponsesTraceDocument("mutual_exclusive_error/response.json");
HttpClient client = await this.CreateTestServerAsync("mutual-exclusive-agent", "You are a helpful assistant.", "Test response");
// Act - Send request with mutually exclusive parameters
HttpResponseMessage httpResponse = await this.SendResponsesRequestAsync(client, "mutual-exclusive-agent", requestJson);
using var responseDoc = await ParseResponseAsync(httpResponse);
var response = responseDoc.RootElement;
// Assert - Should return 400
Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode);
// Assert - Error response structure
AssertJsonPropertyExists(response, "error");
var error = response.GetProperty("error");
AssertJsonPropertyExists(error, "message");
AssertJsonPropertyExists(error, "type");
AssertJsonPropertyExists(error, "code");
var errorMessage = error.GetProperty("message").GetString();
Assert.NotNull(errorMessage);
Assert.Contains("mutually exclusive", errorMessage, StringComparison.OrdinalIgnoreCase);
}
private static List<JsonElement> ParseSseEventsFromContent(string sseContent)
{
var events = new List<JsonElement>();
@@ -1110,7 +1110,7 @@ public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.Services.AddOpenAIResponses();
builder.AddOpenAIResponses();
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
this._app = builder.Build();
@@ -25,7 +25,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("basic/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -41,9 +41,9 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string originalJson = LoadResponsesTraceFile("basic/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.CreateResponse);
string reserializedJson = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.CreateResponse);
string reserializedJson = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse);
CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -59,7 +59,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -74,7 +74,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("conversation/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -89,7 +89,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("metadata/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -142,7 +142,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
};
// Act
string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse);
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -151,7 +151,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.True(root.TryGetProperty("input", out var input));
// Input can be string or object - verify one exists
Assert.True(input.ValueKind == JsonValueKind.String || input.ValueKind == JsonValueKind.Object);
Assert.True(input.ValueKind is JsonValueKind.String or JsonValueKind.Object);
}
[Fact]
@@ -175,7 +175,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
};
// Act
string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse);
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -203,7 +203,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
};
// Act
string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse);
string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -223,7 +223,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("image_input/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -237,7 +237,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("image_input_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -252,7 +252,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("json_output/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -263,7 +263,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format;
Assert.Equal("json_schema", jsonSchemaFormat.Type);
Assert.NotNull(jsonSchemaFormat.Name);
Assert.NotNull(jsonSchemaFormat.Schema);
Assert.NotEqual(default, jsonSchemaFormat.Schema);
}
[Fact]
@@ -273,7 +273,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("json_output_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -293,7 +293,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("reasoning/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -307,7 +307,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("reasoning_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -322,7 +322,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("refusal/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -336,7 +336,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("refusal_streaming/request.json");
// Act
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
// Assert
Assert.NotNull(request);
@@ -370,7 +370,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile(path);
// Act & Assert - Should not throw
CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse);
CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse);
Assert.NotNull(request);
Assert.NotNull(request.Input);
}
@@ -387,7 +387,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -406,7 +406,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -417,7 +417,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.NotNull(outputItem);
// Verify it's a message type
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, Responses.ResponsesJsonContext.Default.ItemResource));
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, OpenAIHostingJsonContext.Default.ItemResource));
var root = doc.RootElement;
Assert.Equal("message", root.GetProperty("type").GetString());
}
@@ -429,7 +429,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("basic/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -448,7 +448,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("conversation/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -464,7 +464,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -481,7 +481,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -497,7 +497,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -512,7 +512,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("metadata/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -528,7 +528,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("tool_call/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -536,7 +536,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.Single(response.Output);
// Verify the output is a function_call type
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], Responses.ResponsesJsonContext.Default.ItemResource));
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], OpenAIHostingJsonContext.Default.ItemResource));
var root = doc.RootElement;
Assert.Equal("function_call", root.GetProperty("type").GetString());
Assert.Equal("get_weather", root.GetProperty("name").GetString());
@@ -552,7 +552,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("tool_call/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -576,7 +576,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("image_input/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -591,7 +591,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("json_output/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -611,7 +611,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("reasoning/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -627,7 +627,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile("refusal/response.json");
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -656,7 +656,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string json = LoadResponsesTraceFile(path);
// Act
Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -675,9 +675,9 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
string originalJson = LoadResponsesTraceFile("basic/response.json");
// Act - Deserialize and re-serialize
Response? response = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.Response);
string reserializedJson = JsonSerializer.Serialize(response, Responses.ResponsesJsonContext.Default.Response);
Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.Response);
Response? response = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.Response);
string reserializedJson = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.Response);
Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.Response);
// Assert
Assert.NotNull(response);
@@ -742,7 +742,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = createdEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -764,7 +764,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = inProgressEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -785,7 +785,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = itemAddedJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -805,7 +805,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = partAddedJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -827,7 +827,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = textDeltaJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -853,7 +853,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
if (evt is StreamingOutputTextDelta delta)
{
@@ -885,7 +885,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
sequenceNumbers.Add(evt.SequenceNumber);
}
@@ -910,15 +910,15 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Act
string jsonString = lastEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
// Should be one of the terminal events
bool isTerminal = evt is StreamingResponseCompleted ||
evt is StreamingResponseIncomplete ||
evt is StreamingResponseFailed;
bool isTerminal = evt is StreamingResponseCompleted or
StreamingResponseIncomplete or
StreamingResponseFailed;
Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}");
}
@@ -935,7 +935,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.NotEmpty(events);
Assert.All(events, evt =>
{
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(parsed);
});
}
@@ -953,7 +953,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.NotEmpty(events);
Assert.All(events, evt =>
{
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(parsed);
});
}
@@ -974,7 +974,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.Contains("response.created", eventTypes);
Assert.All(events, evt =>
{
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(parsed);
});
}
@@ -994,7 +994,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
// Should have refusal-related events
Assert.All(events, evt =>
{
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(parsed);
});
}
@@ -1020,7 +1020,7 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var eventJson in ParseSseEventsFromContent(sseContent))
{
// Should not throw
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
}
}
@@ -1036,24 +1036,24 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
foreach (var eventJson in ParseSseEventsFromContent(sseContent))
{
// Should not throw
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
// Verify polymorphic deserialization worked
Assert.True(
evt is StreamingResponseCreated ||
evt is StreamingResponseInProgress ||
evt is StreamingResponseCompleted ||
evt is StreamingResponseIncomplete ||
evt is StreamingResponseFailed ||
evt is StreamingOutputItemAdded ||
evt is StreamingOutputItemDone ||
evt is StreamingContentPartAdded ||
evt is StreamingContentPartDone ||
evt is StreamingOutputTextDelta ||
evt is StreamingOutputTextDone ||
evt is StreamingFunctionCallArgumentsDelta ||
evt is StreamingFunctionCallArgumentsDone,
evt is StreamingResponseCreated or
StreamingResponseInProgress or
StreamingResponseCompleted or
StreamingResponseIncomplete or
StreamingResponseFailed or
StreamingOutputItemAdded or
StreamingOutputItemDone or
StreamingContentPartAdded or
StreamingContentPartDone or
StreamingOutputTextDelta or
StreamingOutputTextDone or
StreamingFunctionCallArgumentsDelta or
StreamingFunctionCallArgumentsDone,
$"Unknown event type: {evt.GetType().Name}");
}
}
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for SortOrderExtensions.
/// </summary>
public sealed class SortOrderExtensionsTests
{
[Fact]
public void ToOrderString_Ascending_ReturnsAsc()
{
// Arrange
const SortOrder Order = SortOrder.Ascending;
// Act
string result = Order.ToOrderString();
// Assert
Assert.Equal("asc", result);
}
[Fact]
public void ToOrderString_Descending_ReturnsDesc()
{
// Arrange
const SortOrder Order = SortOrder.Descending;
// Act
string result = Order.ToOrderString();
// Assert
Assert.Equal("desc", result);
}
[Fact]
public void IsAscending_Ascending_ReturnsTrue()
{
// Arrange
const SortOrder Order = SortOrder.Ascending;
// Act
bool result = Order.IsAscending();
// Assert
Assert.True(result);
}
[Fact]
public void IsAscending_Descending_ReturnsFalse()
{
// Arrange
const SortOrder Order = SortOrder.Descending;
// Act
bool result = Order.IsAscending();
// Assert
Assert.False(result);
}
}
@@ -108,9 +108,9 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Assert - Last event should be a terminal state
string lastEventType = eventTypes[^1];
Assert.True(
lastEventType == "response.completed" ||
lastEventType == "response.incomplete" ||
lastEventType == "response.failed",
lastEventType is "response.completed" or
"response.incomplete" or
"response.failed",
$"Last event should be a terminal state, got: {lastEventType}");
}
@@ -135,7 +135,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = createdEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -168,7 +168,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = inProgressEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -200,7 +200,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = itemAddedJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -231,7 +231,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = partAddedJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -264,7 +264,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = textDeltaJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
@@ -301,7 +301,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
if (evt is StreamingOutputTextDelta delta)
{
@@ -344,7 +344,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
sequenceNumbers.Add(evt.SequenceNumber);
}
@@ -380,15 +380,15 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Act
string jsonString = lastEventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
// Assert
Assert.NotNull(evt);
// Should be one of the terminal events
bool isTerminal = evt is StreamingResponseCompleted ||
evt is StreamingResponseIncomplete ||
evt is StreamingResponseFailed;
bool isTerminal = evt is StreamingResponseCompleted or
StreamingResponseIncomplete or
StreamingResponseFailed;
Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}");
}
@@ -413,24 +413,24 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in ParseSseEvents(sseContent))
{
// Should not throw
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
// Verify polymorphic deserialization worked
Assert.True(
evt is StreamingResponseCreated ||
evt is StreamingResponseInProgress ||
evt is StreamingResponseCompleted ||
evt is StreamingResponseIncomplete ||
evt is StreamingResponseFailed ||
evt is StreamingOutputItemAdded ||
evt is StreamingOutputItemDone ||
evt is StreamingContentPartAdded ||
evt is StreamingContentPartDone ||
evt is StreamingOutputTextDelta ||
evt is StreamingOutputTextDone ||
evt is StreamingFunctionCallArgumentsDelta ||
evt is StreamingFunctionCallArgumentsDone,
evt is StreamingResponseCreated or
StreamingResponseInProgress or
StreamingResponseCompleted or
StreamingResponseIncomplete or
StreamingResponseFailed or
StreamingOutputItemAdded or
StreamingOutputItemDone or
StreamingContentPartAdded or
StreamingContentPartDone or
StreamingOutputTextDelta or
StreamingOutputTextDone or
StreamingFunctionCallArgumentsDelta or
StreamingFunctionCallArgumentsDone,
$"Unknown event type: {evt.GetType().Name}");
}
}
@@ -459,7 +459,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
string? responseId = null;
@@ -499,7 +499,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
string? itemId = evt switch
{
@@ -545,7 +545,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
// Assert - All events with output_index should have valid values
foreach (var eventJson in ParseSseEvents(await httpResponse.Content.ReadAsStringAsync()))
{
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
if (evt is StreamingOutputItemAdded or StreamingOutputItemDone or StreamingContentPartAdded or StreamingContentPartDone or
@@ -607,7 +607,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
if (evt is StreamingResponseCreated created)
@@ -722,7 +722,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
switch (evt)
@@ -774,7 +774,7 @@ public sealed class StreamingEventConformanceTests : ConformanceTestBase
foreach (var eventJson in events)
{
string jsonString = eventJson.GetRawText();
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent);
StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent);
Assert.NotNull(evt);
Assert.True(sequenceNumbers.Add(evt.SequenceNumber),
@@ -92,6 +92,102 @@ internal static class TestHelpers
}
}
/// <summary>
/// Stateful mock implementation of IChatClient that returns different responses for each call.
/// </summary>
internal sealed class StatefulMockChatClient : IChatClient
{
private readonly string[] _responseTexts;
private int _callIndex;
public StatefulMockChatClient(string[] responseTexts)
{
this._responseTexts = responseTexts;
this._callIndex = 0;
}
public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model");
public Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
// Get the response text for this call
string responseText = this._callIndex < this._responseTexts.Length
? this._responseTexts[this._callIndex]
: this._responseTexts[this._responseTexts.Length - 1];
this._callIndex++;
// Count input messages to simulate context size
int messageCount = messages.Count();
ChatMessage message = new(ChatRole.Assistant, responseText);
ChatResponse response = new([message])
{
ModelId = "test-model",
FinishReason = ChatFinishReason.Stop,
Usage = new UsageDetails
{
InputTokenCount = 10 + (messageCount * 5), // More messages = more tokens
OutputTokenCount = 5,
TotalTokenCount = 15 + (messageCount * 5)
}
};
return Task.FromResult(response);
}
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken);
// Get the response text for this call
string responseText = this._callIndex < this._responseTexts.Length
? this._responseTexts[this._callIndex]
: this._responseTexts[this._responseTexts.Length - 1];
this._callIndex++;
// Count input messages to simulate context size
int messageCount = messages.Count();
// Split response into words to simulate streaming
string[] words = responseText.Split(' ');
for (int i = 0; i < words.Length; i++)
{
string content = i < words.Length - 1 ? words[i] + " " : words[i];
ChatResponseUpdate update = new()
{
Contents = [new TextContent(content)],
Role = ChatRole.Assistant
};
// Add usage to the last update
if (i == words.Length - 1)
{
update.Contents.Add(new UsageContent(new UsageDetails
{
InputTokenCount = 10 + (messageCount * 5),
OutputTokenCount = 5,
TotalTokenCount = 15 + (messageCount * 5)
}));
}
yield return update;
}
}
public object? GetService(Type serviceType, object? serviceKey = null) =>
serviceType.IsInstanceOfType(this) ? this : null;
public void Dispose()
{
}
}
/// <summary>
/// Mock implementation of IChatClient that returns responses with image content.
/// </summary>
@@ -256,7 +352,7 @@ internal static class TestHelpers
public FunctionCallMockChatClient(string functionName = "test_function", string arguments = "{\"param\":\"value\"}")
{
this._functionName = functionName;
this._arguments = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object?>>(arguments) ?? new Dictionary<string, object?>();
this._arguments = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object?>>(arguments) ?? [];
}
public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model");
@@ -408,7 +504,89 @@ internal static class TestHelpers
}
/// <summary>
/// Mock implementation of IChatClient that returns custom content based on a provider function.
/// Mock implementation of IChatClient that returns function call content for tool testing.
/// </summary>
internal sealed class ToolCallMockChatClient : IChatClient
{
private readonly string _functionName;
private readonly Dictionary<string, object?> _arguments;
public ToolCallMockChatClient(string functionName, string argumentsJson)
{
this._functionName = functionName;
// Parse JSON arguments into dictionary
using var doc = System.Text.Json.JsonDocument.Parse(argumentsJson);
this._arguments = new Dictionary<string, object?>();
foreach (var prop in doc.RootElement.EnumerateObject())
{
this._arguments[prop.Name] = prop.Value.ValueKind switch
{
System.Text.Json.JsonValueKind.String => prop.Value.GetString(),
System.Text.Json.JsonValueKind.Number => prop.Value.GetDouble(),
System.Text.Json.JsonValueKind.True => true,
System.Text.Json.JsonValueKind.False => false,
System.Text.Json.JsonValueKind.Null => null,
_ => prop.Value.ToString()
};
}
}
public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model");
public Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
int messageCount = messages.Count();
FunctionCallContent functionCall = new("call_test123", this._functionName, this._arguments);
ChatMessage message = new(ChatRole.Assistant, [functionCall]);
ChatResponse response = new([message])
{
ModelId = "test-model",
FinishReason = ChatFinishReason.ToolCalls,
Usage = new UsageDetails
{
InputTokenCount = 10 + (messageCount * 5),
OutputTokenCount = 5,
TotalTokenCount = 15 + (messageCount * 5)
}
};
return Task.FromResult(response);
}
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken);
int messageCount = messages.Count();
FunctionCallContent functionCall = new("call_test123", this._functionName, this._arguments);
yield return new ChatResponseUpdate
{
Contents = [functionCall, new UsageContent(new UsageDetails
{
InputTokenCount = 10 + (messageCount * 5),
OutputTokenCount = 5,
TotalTokenCount = 15 + (messageCount * 5)
})],
Role = ChatRole.Assistant
};
}
public object? GetService(Type serviceType, object? serviceKey = null) =>
serviceType.IsInstanceOfType(this) ? this : null;
public void Dispose()
{
}
}
/// <summary>
/// Custom content mock implementation of IChatClient that returns custom content based on a provider function.
/// </summary>
internal sealed class CustomContentMockChatClient : IChatClient
{
@@ -22,6 +22,11 @@ internal static class Step7EntryPoint
AgentThread thread = agent.GetNewThread();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread).ConfigureAwait(false))
{
if (update.RawRepresentation is WorkflowEvent)
{
// Skip workflow status updates
continue;
}
string updateText = $"{update.AuthorName
?? update.AgentId
?? update.Role.ToString()