mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
+92
@@ -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();
|
||||
|
||||
+24
@@ -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!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+32
@@ -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"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test_type": "basic_conversation"
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
|
||||
"object": "conversation",
|
||||
"created_at": 1761318654,
|
||||
"metadata": {
|
||||
"test_type": "basic_conversation"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": "gpt-4o-mini",
|
||||
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
|
||||
"input": "What is the capital of France?",
|
||||
"max_output_tokens": 100
|
||||
}
|
||||
+70
@@ -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": {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": "gpt-4o-mini",
|
||||
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
|
||||
"input": "What is its population?",
|
||||
"max_output_tokens": 150
|
||||
}
|
||||
+70
@@ -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": {}
|
||||
}
|
||||
+624
@@ -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 painting’s 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 painting’s 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 painting’s 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 painting’s 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":{}}}
|
||||
|
||||
+17
@@ -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?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "conv_68fb980bccfc8195a9ba32b164e8a69408e61fbaa91b0a18",
|
||||
"object": "conversation",
|
||||
"created_at": 1761318923,
|
||||
"metadata": {
|
||||
"test_type": "create_with_initial_items"
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8",
|
||||
"object": "conversation.deleted",
|
||||
"deleted": true
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
|
||||
"object": "conversation.item.deleted",
|
||||
"deleted": true
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Conversation with id 'conv_nonexistent123' not found.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": null
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Conversation with id 'conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8' not found.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": null
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test": "invalid"
|
||||
}
|
||||
// missing closing brace and has comment which is invalid JSON
|
||||
+8
@@ -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"
|
||||
}
|
||||
}
|
||||
+8
@@ -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"
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "Item with id 'msg_msg_nonexistent123nonexistent123' not found in conversation.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": null
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "message",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test_type": "image_input_conversation"
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7",
|
||||
"object": "conversation",
|
||||
"created_at": 1761319071,
|
||||
"metadata": {
|
||||
"test_type": "image_input_conversation"
|
||||
}
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+70
@@ -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": {}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test_type": "image_input_streaming"
|
||||
}
|
||||
}
|
||||
+22
@@ -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
|
||||
}
|
||||
+456
@@ -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":{}}}
|
||||
|
||||
+86
@@ -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"
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a",
|
||||
"object": "conversation",
|
||||
"created_at": 1761319238,
|
||||
"metadata": {
|
||||
"test_type": "refusal"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": "gpt-4o-mini",
|
||||
"conversation": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a",
|
||||
"input": "How can I create a computer virus?",
|
||||
"max_output_tokens": 100
|
||||
}
|
||||
+70
@@ -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": {}
|
||||
}
|
||||
+7
@@ -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
|
||||
}
|
||||
+54
@@ -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":{}}}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
|
||||
"object": "conversation",
|
||||
"created_at": 1761318654,
|
||||
"metadata": {
|
||||
"test_type": "basic_conversation"
|
||||
}
|
||||
}
|
||||
+14
@@ -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"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test_type": "tool_call_conversation"
|
||||
}
|
||||
}
|
||||
+27
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+92
@@ -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": {}
|
||||
}
|
||||
+28
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"metadata": {
|
||||
"test_type": "basic_conversation",
|
||||
"updated": "true",
|
||||
"update_timestamp": "2025-10-24"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
|
||||
"object": "conversation",
|
||||
"created_at": 1761318654,
|
||||
"metadata": {
|
||||
"test_type": "basic_conversation",
|
||||
"updated": "true",
|
||||
"update_timestamp": "2025-10-24"
|
||||
}
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
+8
@@ -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"
|
||||
}
|
||||
}
|
||||
+57
-1
@@ -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
|
||||
}
|
||||
-78
@@ -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);
|
||||
}
|
||||
}
|
||||
+359
@@ -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);
|
||||
}
|
||||
}
|
||||
+645
@@ -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);
|
||||
}
|
||||
}
|
||||
+1201
File diff suppressed because it is too large
Load Diff
+592
@@ -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
|
||||
}
|
||||
+435
@@ -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);
|
||||
}
|
||||
}
|
||||
+439
@@ -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();
|
||||
}
|
||||
}
|
||||
+110
-304
@@ -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>();
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+71
-71
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
-34
@@ -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
|
||||
{
|
||||
|
||||
+5
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user