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

* Improve fidelity of OpenAI Responses server and add Conversations

* Merge

* nit

* Undo prior change

* Undo prior change

* Review feedback

* Review feedback

* Fix test

* Use simpler JsonDocument approach for polymorphic deserialization

* More review feedback

* dotnet format
This commit is contained in:
Reuben Bond
2025-11-05 18:46:19 +00:00
committed by GitHub
parent e2282ebe42
commit 33f84f9ed2
135 changed files with 11718 additions and 1243 deletions
+1
View File
@@ -53,6 +53,7 @@
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.10.2" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.10.0-preview.1.25513.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.10.2-preview.1.25552.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
@@ -68,7 +68,7 @@ internal static class AIAgentChatCompletionsProcessor
{
// The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
DateTimeOffset? createdAt = null;
var chunkId = IdGeneratorHelpers.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13);
var chunkId = IdGenerator.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13);
await foreach (var agentRunResponseUpdate in agent.RunStreamingAsync(chatMessages, options: options, cancellationToken: cancellationToken).WithCancellation(cancellationToken))
{
@@ -20,7 +20,7 @@ internal static class AgentRunResponseExtensions
return new ChatCompletion
{
Id = IdGeneratorHelpers.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13),
Id = IdGenerator.NewId(prefix: "chatcmpl", delimiter: "-", stringLength: 13),
Choices = choices,
Created = (agentRunResponse.CreatedAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
Model = request.Model,
@@ -8,6 +8,14 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Converters;
internal static class MessageContentPartConverter
{
private static string AudioFormatToMediaType(string format) =>
format.Equals("mp3", StringComparison.OrdinalIgnoreCase) ? "audio/mpeg" :
format.Equals("wav", StringComparison.OrdinalIgnoreCase) ? "audio/wav" :
format.Equals("opus", StringComparison.OrdinalIgnoreCase) ? "audio/opus" :
format.Equals("aac", StringComparison.OrdinalIgnoreCase) ? "audio/aac" :
format.Equals("flac", StringComparison.OrdinalIgnoreCase) ? "audio/flac" :
format.Equals("pcm16", StringComparison.OrdinalIgnoreCase) ? "audio/pcm" :
"audio/*";
public static AIContent? ToAIContent(MessageContentPart part)
{
return part switch
@@ -23,16 +31,7 @@ internal static class MessageContentPartConverter
// audio
AudioContentPart audioPart =>
new DataContent(audioPart.InputAudio.Data, audioPart.InputAudio.Format.ToUpperInvariant() switch
{
"MP3" => "audio/mpeg",
"WAV" => "audio/wav",
"OPUS" => "audio/opus",
"AAC" => "audio/aac",
"FLAC" => "audio/flac",
"PCM16" => "audio/pcm",
_ => "audio/*"
}),
new DataContent(audioPart.InputAudio.Data, AudioFormatToMediaType(audioPart.InputAudio.Format)),
// file
FileContentPart filePart when !string.IsNullOrEmpty(filePart.File.FileId)
@@ -0,0 +1,342 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
/// <summary>
/// Handles route requests for OpenAI Conversations API endpoints.
/// </summary>
internal sealed class ConversationsHttpHandler
{
private readonly IConversationStorage _storage;
private readonly IAgentConversationIndex? _conversationIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ConversationsHttpHandler"/> class.
/// </summary>
/// <param name="storage">The conversation storage service.</param>
/// <param name="conversationIndex">Optional conversation index service.</param>
public ConversationsHttpHandler(IConversationStorage storage, IAgentConversationIndex? conversationIndex)
{
this._storage = storage ?? throw new ArgumentNullException(nameof(storage));
this._conversationIndex = conversationIndex;
}
/// <summary>
/// Lists conversations by agent ID.
/// </summary>
public async Task<IResult> ListConversationsByAgentAsync(
[FromQuery] string? agent_id,
CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(agent_id))
{
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = "agent_id query parameter is required.",
Type = "invalid_request_error"
}
});
}
// Return empty list if conversation index is not registered
if (this._conversationIndex == null)
{
return Results.Ok(new ListResponse<Conversation>
{
Data = [],
HasMore = false
});
}
var conversationIdsResponse = await this._conversationIndex.GetConversationIdsAsync(agent_id, cancellationToken).ConfigureAwait(false);
// Fetch full conversation objects
var conversations = new List<Conversation>();
foreach (var conversationId in conversationIdsResponse.Data)
{
var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (conversation is not null)
{
conversations.Add(conversation);
}
}
return Results.Ok(new ListResponse<Conversation>
{
Data = conversations,
HasMore = false
});
}
/// <summary>
/// Creates a new conversation.
/// </summary>
public async Task<IResult> CreateConversationAsync(
[FromBody] CreateConversationRequest request,
CancellationToken cancellationToken)
{
Dictionary<string, string> metadata = request.Metadata ?? [];
var idGenerator = new IdGenerator(responseId: null, conversationId: null);
var conversation = new Conversation
{
Id = idGenerator.ConversationId,
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Metadata = metadata
};
var created = await this._storage.CreateConversationAsync(conversation, cancellationToken).ConfigureAwait(false);
// Add initial items if provided
if (request.Items is { Count: > 0 })
{
List<ItemResource> itemsToAdd = [.. request.Items.Select(itemParam => itemParam.ToItemResource(idGenerator))];
await this._storage.AddItemsAsync(created.Id, itemsToAdd, cancellationToken).ConfigureAwait(false);
}
// Add to conversation index if available and agent_id is provided in metadata
if (this._conversationIndex != null && created.Metadata.TryGetValue("agent_id", out var agentId) && !string.IsNullOrEmpty(agentId))
{
await this._conversationIndex.AddConversationAsync(agentId, created.Id, cancellationToken).ConfigureAwait(false);
}
return Results.Ok(created);
}
/// <summary>
/// Retrieves a conversation by ID.
/// </summary>
public async Task<IResult> GetConversationAsync(
string conversationId,
CancellationToken cancellationToken)
{
var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
return conversation is not null
? Results.Ok(conversation)
: Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Conversation '{conversationId}' not found.",
Type = "invalid_request_error"
}
});
}
/// <summary>
/// Updates a conversation's metadata.
/// </summary>
public async Task<IResult> UpdateConversationAsync(
string conversationId,
[FromBody] UpdateConversationRequest request,
CancellationToken cancellationToken)
{
var existing = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (existing is null)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Conversation '{conversationId}' not found.",
Type = "invalid_request_error"
}
});
}
var updated = existing with
{
Metadata = request.Metadata
};
var result = await this._storage.UpdateConversationAsync(updated, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
}
/// <summary>
/// Deletes a conversation and all its messages.
/// </summary>
public async Task<IResult> DeleteConversationAsync(
string conversationId,
CancellationToken cancellationToken)
{
// Get conversation first to retrieve agent_id for index removal
var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
var deleted = await this._storage.DeleteConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (!deleted)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Conversation '{conversationId}' not found.",
Type = "invalid_request_error"
}
});
}
// Remove from conversation index if available and agent_id was present in metadata
if (this._conversationIndex != null && conversation?.Metadata.TryGetValue("agent_id", out var agentId) == true && !string.IsNullOrEmpty(agentId))
{
await this._conversationIndex.RemoveConversationAsync(agentId, conversationId, cancellationToken).ConfigureAwait(false);
}
return Results.Ok(new DeleteResponse
{
Id = conversationId,
Object = "conversation.deleted",
Deleted = true
});
}
/// <summary>
/// Adds items to a conversation.
/// </summary>
public async Task<IResult> CreateItemsAsync(
string conversationId,
[FromBody] CreateItemsRequest request,
[FromQuery] string[]? include,
CancellationToken cancellationToken)
{
var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (conversation is null)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Conversation '{conversationId}' not found.",
Type = "invalid_request_error"
}
});
}
var idGenerator = new IdGenerator(responseId: null, conversationId: conversationId);
List<ItemResource> createdItems = [.. request.Items.Select(itemParam => itemParam.ToItemResource(idGenerator))];
await this._storage.AddItemsAsync(conversationId, createdItems, cancellationToken).ConfigureAwait(false);
return Results.Ok(new ListResponse<ItemResource>
{
Data = createdItems,
FirstId = createdItems.Count > 0 ? createdItems[0].Id : null,
LastId = createdItems.Count > 0 ? createdItems[^1].Id : null,
HasMore = false
});
}
/// <summary>
/// Lists items in a conversation.
/// </summary>
public async Task<IResult> ListItemsAsync(
string conversationId,
[FromQuery] int? limit,
[FromQuery] string? order,
[FromQuery] string? after,
[FromQuery] string[]? include,
CancellationToken cancellationToken)
{
// Validate limit parameter
if (limit is < 1)
{
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = "Invalid value for 'limit': must be a positive integer.",
Type = "invalid_request_error",
Code = "invalid_value"
}
});
}
var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (conversation is null)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Conversation '{conversationId}' not found.",
Type = "invalid_request_error"
}
});
}
var result = await this._storage.ListItemsAsync(conversationId, limit, ParseOrder(order), after, cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
}
/// <summary>
/// Retrieves a specific item.
/// </summary>
public async Task<IResult> GetItemAsync(
string conversationId,
string itemId,
[FromQuery] string[]? include,
CancellationToken cancellationToken)
{
var item = await this._storage.GetItemAsync(conversationId, itemId, cancellationToken).ConfigureAwait(false);
return item is not null
? Results.Ok(item)
: Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Item '{itemId}' not found in conversation '{conversationId}'.",
Type = "invalid_request_error"
}
});
}
/// <summary>
/// Deletes a specific item.
/// </summary>
public async Task<IResult> DeleteItemAsync(
string conversationId,
string itemId,
CancellationToken cancellationToken)
{
var deleted = await this._storage.DeleteItemAsync(conversationId, itemId, cancellationToken).ConfigureAwait(false);
if (!deleted)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Item '{itemId}' not found in conversation '{conversationId}'.",
Type = "invalid_request_error"
}
});
}
return Results.Ok(new DeleteResponse
{
Id = itemId,
Object = "conversation.item.deleted",
Deleted = true
});
}
private static SortOrder? ParseOrder(string? order)
{
if (order is null)
{
return null;
}
return string.Equals(order, "asc", StringComparison.OrdinalIgnoreCase) ? SortOrder.Ascending : SortOrder.Descending;
}
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
/// <summary>
/// Optional service for indexing conversations by agent ID.
/// This is a non-standard extension to the OpenAI Conversations API.
/// </summary>
internal interface IAgentConversationIndex
{
/// <summary>
/// Adds a conversation to the index for the specified agent.
/// </summary>
/// <param name="agentId">The agent identifier.</param>
/// <param name="conversationId">The conversation identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default);
/// <summary>
/// Removes a conversation from the index for the specified agent.
/// </summary>
/// <param name="agentId">The agent identifier.</param>
/// <param name="conversationId">The conversation identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RemoveConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all conversation IDs for the specified agent.
/// </summary>
/// <param name="agentId">The agent identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list response containing conversation IDs associated with the agent.</returns>
Task<ListResponse<string>> GetConversationIdsAsync(string agentId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
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.Conversations;
/// <summary>
/// Storage abstraction for conversations and messages.
/// This interface provides operations specifically designed for conversation management,
/// going beyond simple key-value storage to support conversation-specific queries and operations.
/// </summary>
internal interface IConversationStorage
{
/// <summary>
/// Creates a new conversation.
/// </summary>
/// <param name="conversation">The conversation to create.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created conversation.</returns>
Task<Conversation> CreateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a conversation by ID.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The conversation if found, null otherwise.</returns>
Task<Conversation?> GetConversationAsync(string conversationId, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing conversation.
/// </summary>
/// <param name="conversation">The conversation with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The updated conversation if found, null otherwise.</returns>
Task<Conversation?> UpdateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a conversation and all its messages.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if deleted, false if not found.</returns>
Task<bool> DeleteConversationAsync(string conversationId, CancellationToken cancellationToken = default);
// Item operations
/// <summary>
/// Adds multiple items to a conversation atomically.
/// Items are ItemResource objects from the Responses API.
/// </summary>
/// <param name="conversationId">The conversation ID to add the items to.</param>
/// <param name="items">The items to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that completes when all items have been added.</returns>
Task AddItemsAsync(string conversationId, IEnumerable<ItemResource> items, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves an item by ID.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="itemId">The item ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item if found, null otherwise.</returns>
Task<ItemResource?> GetItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default);
/// <summary>
/// Lists items in a conversation with pagination support.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="limit">Maximum number of items to return (default: 20, max: 100).</param>
/// <param name="order">Sort order (default: Descending).</param>
/// <param name="after">Cursor for pagination - return items after this ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list response with items and pagination info.</returns>
Task<ListResponse<ItemResource>> ListItemsAsync(
string conversationId,
int? limit = null,
SortOrder? order = null,
string? after = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a specific item from a conversation.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="itemId">The item ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if deleted, false if not found.</returns>
Task<bool> DeleteItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
/// <summary>
/// In-memory implementation of IAgentConversationIndex for development and testing.
/// This is a non-standard extension to the OpenAI Conversations API.
/// </summary>
internal sealed class InMemoryAgentConversationIndex : IAgentConversationIndex, IDisposable
{
private readonly MemoryCache _cache;
private readonly InMemoryStorageOptions _options;
private sealed class ConversationSet
{
private readonly HashSet<string> _conversations = [];
private readonly object _lock = new();
public void Add(string conversationId)
{
lock (this._lock)
{
this._conversations.Add(conversationId);
}
}
public bool Remove(string conversationId)
{
lock (this._lock)
{
return this._conversations.Remove(conversationId);
}
}
public string[] GetAll()
{
lock (this._lock)
{
return [.. this._conversations];
}
}
}
public InMemoryAgentConversationIndex()
: this(new InMemoryStorageOptions())
{
}
public InMemoryAgentConversationIndex(InMemoryStorageOptions options)
{
ArgumentNullException.ThrowIfNull(options);
this._options = options;
this._cache = new MemoryCache(options.ToMemoryCacheOptions());
}
private async Task<ConversationSet> GetOrCreateConversationSetAsync(string agentId, CancellationToken cancellationToken)
{
var conversationSet = await this._cache.GetOrCreateAtomicAsync(
agentId,
entry =>
{
entry.SetOptions(this._options.ToMemoryCacheEntryOptions());
return new ConversationSet();
},
cancellationToken).ConfigureAwait(false);
return conversationSet!;
}
/// <inheritdoc />
public async Task AddConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(agentId);
ArgumentException.ThrowIfNullOrEmpty(conversationId);
ConversationSet conversationSet = await this.GetOrCreateConversationSetAsync(agentId, cancellationToken).ConfigureAwait(false);
conversationSet.Add(conversationId);
}
/// <inheritdoc />
public async Task RemoveConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(agentId);
ArgumentException.ThrowIfNullOrEmpty(conversationId);
if (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null)
{
conversationSet.Remove(conversationId);
}
}
/// <inheritdoc/>
public async Task<ListResponse<string>> GetConversationIdsAsync(string agentId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(agentId);
string[] conversations = (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null)
? conversationSet.GetAll()
: [];
return new ListResponse<string>
{
Data = [.. conversations],
HasMore = false
};
}
public void Dispose()
{
// The MemoryCache will call the post-eviction callbacks when disposed,
// which will dispose all ConversationSet instances
this._cache.Dispose();
}
}
@@ -0,0 +1,346 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
/// <summary>
/// In-memory implementation of conversation storage for testing and development.
/// This implementation is thread-safe but data is not persisted across application restarts.
/// </summary>
internal sealed class InMemoryConversationStorage : IConversationStorage, IDisposable
{
private const int DefaultListItemLimit = 20;
private readonly MemoryCache _cache;
private readonly InMemoryStorageOptions _options;
public InMemoryConversationStorage()
: this(new InMemoryStorageOptions())
{
}
public InMemoryConversationStorage(InMemoryStorageOptions options)
{
ArgumentNullException.ThrowIfNull(options);
this._options = options;
this._cache = new MemoryCache(options.ToMemoryCacheOptions());
}
/// <inheritdoc />
public Task<Conversation> CreateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default)
{
// Check if conversation already exists
if (this._cache.TryGetValue(conversation.Id, out ConversationState? _))
{
throw new InvalidOperationException($"Conversation with ID '{conversation.Id}' already exists.");
}
var state = new ConversationState(conversation);
var entryOptions = this._options.ToMemoryCacheEntryOptions();
this._cache.Set(conversation.Id, state, entryOptions);
return Task.FromResult(conversation);
}
/// <inheritdoc />
public Task<Conversation?> GetConversationAsync(string conversationId, CancellationToken cancellationToken = default)
{
if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null)
{
return Task.FromResult<Conversation?>(state.Conversation);
}
return Task.FromResult<Conversation?>(null);
}
/// <inheritdoc />
public Task<Conversation?> UpdateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default)
{
if (this._cache.TryGetValue(conversation.Id, out ConversationState? state) && state is not null)
{
state.UpdateConversation(conversation);
// Touch the cache entry to reset expiration
var entryOptions = this._options.ToMemoryCacheEntryOptions();
this._cache.Set(conversation.Id, state, entryOptions);
return Task.FromResult<Conversation?>(conversation);
}
return Task.FromResult<Conversation?>(null);
}
/// <inheritdoc />
public Task<bool> DeleteConversationAsync(string conversationId, CancellationToken cancellationToken = default)
{
if (this._cache.TryGetValue<ConversationState>(conversationId, out _))
{
this._cache.Remove(conversationId);
return Task.FromResult(true);
}
return Task.FromResult(false);
}
/// <inheritdoc />
public Task AddItemsAsync(string conversationId, IEnumerable<ItemResource> items, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(conversationId, nameof(conversationId));
ArgumentNullException.ThrowIfNull(items);
if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null)
{
throw new InvalidOperationException($"Conversation '{conversationId}' not found.");
}
foreach (ItemResource item in items)
{
state.AddItem(item);
}
// Touch the cache entry to reset expiration
var entryOptions = this._options.ToMemoryCacheEntryOptions();
this._cache.Set(conversationId, state, entryOptions);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<ItemResource?> GetItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default)
{
if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null)
{
return Task.FromResult(state.GetItem(itemId));
}
return Task.FromResult<ItemResource?>(null);
}
/// <inheritdoc/>
public Task<ListResponse<ItemResource>> ListItemsAsync(
string conversationId,
int? limit = null,
SortOrder? order = null,
string? after = null,
CancellationToken cancellationToken = default)
{
int effectiveLimit = Math.Clamp(limit ?? DefaultListItemLimit, 1, 100);
SortOrder effectiveOrder = order ?? SortOrder.Descending;
if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null)
{
throw new InvalidOperationException($"Conversation '{conversationId}' not found.");
}
var allItems = state.GetAllItems();
// For descending order, reverse the list
if (effectiveOrder == SortOrder.Descending)
{
allItems.Reverse();
}
var filtered = allItems.AsEnumerable();
if (!string.IsNullOrEmpty(after))
{
var afterIndex = allItems.FindIndex(m => m.Id == after);
if (afterIndex >= 0)
{
filtered = allItems.Skip(afterIndex + 1);
}
}
List<ItemResource> result;
bool hasMore;
if (filtered.TryGetNonEnumeratedCount(out int count))
{
hasMore = count > effectiveLimit;
result = filtered.Take(effectiveLimit).ToList();
}
else
{
result = filtered.Take(effectiveLimit + 1).ToList();
hasMore = result.Count > effectiveLimit;
if (hasMore)
{
result = result.Take(effectiveLimit).ToList();
}
}
return Task.FromResult(new ListResponse<ItemResource>
{
Data = result,
FirstId = result.FirstOrDefault()?.Id,
LastId = result.LastOrDefault()?.Id,
HasMore = hasMore
});
}
/// <inheritdoc />
public Task<bool> DeleteItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default)
{
if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null)
{
var removed = state.RemoveItem(itemId);
if (removed)
{
// Touch the cache entry to reset expiration
var entryOptions = this._options.ToMemoryCacheEntryOptions();
this._cache.Set(conversationId, state, entryOptions);
}
return Task.FromResult(removed);
}
return Task.FromResult(false);
}
/// <summary>
/// Encapsulates per-conversation state including items storage and synchronization.
/// </summary>
private sealed class ConversationState
{
#if NET9_0_OR_GREATER
private readonly OrderedDictionary<string, ItemResource> _items = [];
private readonly object _lock = new();
private Conversation _conversation;
public ConversationState(Conversation conversation)
{
this._conversation = conversation;
}
public Conversation Conversation
{
get
{
lock (this._lock)
{
return this._conversation;
}
}
}
public void UpdateConversation(Conversation conversation)
{
lock (this._lock)
{
this._conversation = conversation;
}
}
public void AddItem(ItemResource item)
{
lock (this._lock)
{
if (!this._items.TryAdd(item.Id, item))
{
throw new InvalidOperationException($"Item with ID '{item.Id}' already exists.");
}
}
}
public ItemResource? GetItem(string itemId)
{
lock (this._lock)
{
this._items.TryGetValue(itemId, out var item);
return item;
}
}
public List<ItemResource> GetAllItems()
{
lock (this._lock)
{
return this._items.Values.ToList();
}
}
public bool RemoveItem(string itemId)
{
lock (this._lock)
{
return this._items.Remove(itemId);
}
}
#else
private readonly List<ItemResource> _items = [];
private readonly object _lock = new();
private Conversation _conversation;
public ConversationState(Conversation conversation)
{
this._conversation = conversation;
}
public Conversation Conversation
{
get
{
lock (this._lock)
{
return this._conversation;
}
}
}
public void UpdateConversation(Conversation conversation)
{
lock (this._lock)
{
this._conversation = conversation;
}
}
public void AddItem(ItemResource item)
{
lock (this._lock)
{
if (this._items.Exists(i => i.Id == item.Id))
{
throw new InvalidOperationException($"Item with ID '{item.Id}' already exists.");
}
this._items.Add(item);
}
}
public ItemResource? GetItem(string itemId)
{
lock (this._lock)
{
return this._items.Find(i => i.Id == itemId);
}
}
public List<ItemResource> GetAllItems()
{
lock (this._lock)
{
return this._items.ToList();
}
}
public bool RemoveItem(string itemId)
{
lock (this._lock)
{
return this._items.RemoveAll(i => i.Id == itemId) > 0;
}
}
#endif
}
public void Dispose()
{
this._cache.Dispose();
}
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
/// <summary>
/// Request to create items in a conversation.
/// </summary>
internal sealed class CreateItemsRequest
{
/// <summary>
/// The items to add to the conversation. You may add up to 20 items at a time.
/// Items should be ItemParam objects (messages without IDs, function call outputs, etc.).
/// The server will assign IDs when creating the items.
/// </summary>
[JsonPropertyName("items")]
public required List<ItemParam> Items { get; init; }
}
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
/// <summary>
/// Represents a conversation in the system.
/// </summary>
internal sealed record Conversation
{
/// <summary>
/// The unique identifier for the conversation.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// The object type, always "conversation".
/// </summary>
[JsonPropertyName("object")]
[SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")]
public string Object => "conversation";
/// <summary>
/// The Unix timestamp (in seconds) for when the conversation was created.
/// </summary>
[JsonPropertyName("created_at")]
public required long CreatedAt { get; init; }
/// <summary>
/// Set of 16 key-value pairs that can be attached to a conversation.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, string> Metadata { get; init; } = [];
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
/// <summary>
/// Request to create a new conversation.
/// </summary>
internal sealed class CreateConversationRequest
{
/// <summary>
/// Initial items to include in the conversation context. You may add up to 20 items at a time.
/// Items should be ItemParam objects (messages without IDs, as the server will generate them).
/// </summary>
[JsonPropertyName("items")]
public List<ItemParam>? Items { get; init; }
/// <summary>
/// Set of 16 key-value pairs that can be attached to a conversation.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; init; }
}
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models;
/// <summary>
/// Request to update an existing conversation.
/// </summary>
internal sealed class UpdateConversationRequest
{
/// <summary>
/// Set of 16 key-value pairs that can be attached to a conversation.
/// </summary>
[JsonPropertyName("metadata")]
public required Dictionary<string, string> Metadata { get; init; }
}
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
/// <summary>
/// Extension methods for <see cref="SortOrder"/>.
/// </summary>
internal static class SortOrderExtensions
{
/// <summary>
/// Converts a <see cref="SortOrder"/> to its string representation.
/// </summary>
/// <param name="order">The sort order.</param>
/// <returns>The string representation ("asc" or "desc").</returns>
public static string ToOrderString(this SortOrder order)
{
return order == SortOrder.Ascending ? "asc" : "desc";
}
/// <summary>
/// Checks if the sort order is ascending.
/// </summary>
/// <param name="order">The sort order.</param>
/// <returns>True if ascending, false otherwise.</returns>
public static bool IsAscending(this SortOrder order)
{
return order == SortOrder.Ascending;
}
}
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder;
/// <summary>
/// Provides extension methods for mapping OpenAI Conversations API to an <see cref="IEndpointRouteBuilder"/>.
/// </summary>
public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps OpenAI Conversations API endpoints to the specified <see cref="IEndpointRouteBuilder"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI Conversations endpoints to.</param>
public static IEndpointConventionBuilder MapOpenAIConversations(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var storage = endpoints.ServiceProvider.GetService<IConversationStorage>()
?? throw new InvalidOperationException("IConversationStorage is not registered. Call AddOpenAIConversations() in your service configuration.");
var conversationIndex = endpoints.ServiceProvider.GetService<IAgentConversationIndex>();
var handlers = new ConversationsHttpHandler(storage, conversationIndex);
var group = endpoints.MapGroup("/v1/conversations")
.WithTags("Conversations");
// Conversation endpoints
// Non-standard extension: List conversations by agent ID
group.MapGet("", handlers.ListConversationsByAgentAsync)
.WithName("ListConversationsByAgent")
.WithSummary("List conversations for a specific agent (non-standard extension)");
group.MapPost("", handlers.CreateConversationAsync)
.WithName("CreateConversation")
.WithSummary("Create a new conversation");
group.MapGet("{conversationId}", handlers.GetConversationAsync)
.WithName("GetConversation")
.WithSummary("Retrieve a conversation by ID");
group.MapPost("{conversationId}", handlers.UpdateConversationAsync)
.WithName("UpdateConversation")
.WithSummary("Update a conversation's metadata or title");
group.MapDelete("{conversationId}", handlers.DeleteConversationAsync)
.WithName("DeleteConversation")
.WithSummary("Delete a conversation and all its messages");
// Item endpoints
group.MapPost("{conversationId}/items", handlers.CreateItemsAsync)
.WithName("CreateItems")
.WithSummary("Add items to a conversation");
group.MapGet("{conversationId}/items", handlers.ListItemsAsync)
.WithName("ListItems")
.WithSummary("List items in a conversation");
group.MapGet("{conversationId}/items/{itemId}", handlers.GetItemAsync)
.WithName("GetItem")
.WithSummary("Retrieve a specific item");
group.MapDelete("{conversationId}/items/{itemId}", handlers.DeleteItemAsync)
.WithName("DeleteItem")
.WithSummary("Delete a specific item");
return group;
}
}
@@ -2,12 +2,11 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.OpenAI;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -43,11 +42,43 @@ public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExt
ValidateAgentName(agent.Name);
responsesPath ??= $"/{agent.Name}/v1/responses";
// Create an executor for this agent
var executor = new AIAgentResponseExecutor(agent);
var storageOptions = endpoints.ServiceProvider.GetService<InMemoryStorageOptions>() ?? new InMemoryStorageOptions();
var conversationStorage = endpoints.ServiceProvider.GetService<IConversationStorage>();
var responsesService = new InMemoryResponsesService(executor, storageOptions, conversationStorage);
var handlers = new ResponsesHttpHandler(responsesService);
var group = endpoints.MapGroup(responsesPath);
var endpointAgentName = agent.DisplayName;
group.MapPost("/", async ([FromBody] CreateResponse createResponse, CancellationToken cancellationToken)
=> await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false))
.WithName(endpointAgentName + "/CreateResponse");
// Create response endpoint
group.MapPost("/", handlers.CreateResponseAsync)
.WithName(endpointAgentName + "/CreateResponse")
.WithSummary("Creates a model response for the given input");
// Get response endpoint
group.MapGet("{responseId}", handlers.GetResponseAsync)
.WithName(endpointAgentName + "/GetResponse")
.WithSummary("Retrieves a response by ID");
// Cancel response endpoint
group.MapPost("{responseId}/cancel", handlers.CancelResponseAsync)
.WithName(endpointAgentName + "/CancelResponse")
.WithSummary("Cancels an in-progress response");
// Delete response endpoint
group.MapDelete("{responseId}", handlers.DeleteResponseAsync)
.WithName(endpointAgentName + "/DeleteResponse")
.WithSummary("Deletes a response");
// List response input items endpoint
group.MapGet("{responseId}/input_items", handlers.ListResponseInputItemsAsync)
.WithName(endpointAgentName + "/ListResponseInputItems")
.WithSummary("Lists the input items for a response");
return group;
}
@@ -70,24 +101,37 @@ public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExt
ArgumentNullException.ThrowIfNull(endpoints);
responsesPath ??= "/v1/responses";
var responsesService = endpoints.ServiceProvider.GetService<IResponsesService>()
?? throw new InvalidOperationException("IResponsesService is not registered. Call AddOpenAIResponses() in your service configuration.");
var handlers = new ResponsesHttpHandler(responsesService);
var group = endpoints.MapGroup(responsesPath);
group.MapPost("/", async ([FromBody] CreateResponse createResponse, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
// DevUI uses the 'model' field to specify the agent name.
var agentName = createResponse.Agent?.Name ?? createResponse.Model;
if (agentName is null)
{
return Results.BadRequest("No 'agent.name' or 'model' specified in the request.");
}
var agent = serviceProvider.GetKeyedService<AIAgent>(agentName);
if (agent is null)
{
return Results.NotFound($"Agent named '{agentName}' was not found.");
}
// Create response endpoint
group.MapPost("/", handlers.CreateResponseAsync)
.WithName("CreateResponse")
.WithSummary("Creates a model response for the given input");
// Get response endpoint
group.MapGet("{responseId}", handlers.GetResponseAsync)
.WithName("GetResponse")
.WithSummary("Retrieves a response by ID");
// Cancel response endpoint
group.MapPost("{responseId}/cancel", handlers.CancelResponseAsync)
.WithName("CancelResponse")
.WithSummary("Cancels an in-progress response");
// Delete response endpoint
group.MapDelete("{responseId}", handlers.DeleteResponseAsync)
.WithName("DeleteResponse")
.WithSummary("Deletes a response");
// List response input items endpoint
group.MapGet("{responseId}/input_items", handlers.ListResponseInputItemsAsync)
.WithName("ListResponseInputItems")
.WithSummary("Lists the input items for a response");
return await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false);
}).WithName("CreateResponse");
return group;
}
@@ -39,4 +39,18 @@ public static class MicrosoftAgentAIHostingOpenAIHostApplicationBuilderExtension
return builder;
}
/// <summary>
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI Responses.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to configure.</param>
/// <returns>The <see cref="IHostApplicationBuilder"/> for method chaining.</returns>
public static IHostApplicationBuilder AddOpenAIConversations(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddOpenAIConversations();
return builder;
}
}
@@ -0,0 +1,193 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// Generates IDs with partition keys.
/// </summary>
internal sealed partial class IdGenerator
{
private readonly string _partitionId;
private readonly Random? _random;
#if NET9_0_OR_GREATER
[GeneratedRegex("^[A-Za-z0-9]+$")]
private static partial Regex WatermarkRegex();
#else
private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
private static Regex WatermarkRegex() => s_watermarkRegex;
#endif
/// <summary>
/// Initializes a new instance of the <see cref="IdGenerator"/> class.
/// </summary>
/// <param name="responseId">The response ID.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="randomSeed">Optional random seed for deterministic ID generation. When null, uses cryptographically secure random generation.</param>
public IdGenerator(string? responseId, string? conversationId, int? randomSeed = null)
{
this._random = randomSeed.HasValue ? new Random(randomSeed.Value) : null;
this.ResponseId = responseId ?? NewId("resp", random: this._random);
this.ConversationId = conversationId ?? NewId("conv", random: this._random);
this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
}
/// <summary>
/// Creates a new ID generator from a create response request.
/// </summary>
/// <param name="request">The create response request.</param>
/// <returns>A new ID generator.</returns>
public static IdGenerator From(CreateResponse request)
{
string? responseId = null;
request.Metadata?.TryGetValue("response_id", out responseId);
return new IdGenerator(responseId, request.Conversation?.Id);
}
/// <summary>
/// Gets the response ID.
/// </summary>
public string ResponseId { get; }
/// <summary>
/// Gets the conversation ID.
/// </summary>
public string ConversationId { get; }
/// <summary>
/// Generates a new ID.
/// </summary>
/// <param name="category">The optional category for the ID.</param>
/// <returns>A generated ID string.</returns>
public string Generate(string? category = null)
{
var prefix = string.IsNullOrEmpty(category) ? "id" : category;
return NewId(prefix, partitionKey: this._partitionId, random: this._random);
}
/// <summary>
/// Generates a function call ID.
/// </summary>
/// <returns>A function call ID.</returns>
public string GenerateFunctionCallId() => this.Generate("func");
/// <summary>
/// Generates a function output ID.
/// </summary>
/// <returns>A function output ID.</returns>
public string GenerateFunctionOutputId() => this.Generate("funcout");
/// <summary>
/// Generates a message ID.
/// </summary>
/// <returns>A message ID.</returns>
public string GenerateMessageId() => this.Generate("msg");
/// <summary>
/// Generates a reasoning ID.
/// </summary>
/// <returns>A reasoning ID.</returns>
public string GenerateReasoningId() => this.Generate("rs");
/// <summary>
/// Generates a new ID with a structured format that includes a partition key.
/// </summary>
/// <param name="prefix">The prefix to add to the ID, typically indicating the resource type.</param>
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
/// <param name="infix">Optional additional text to insert between the prefix and the entropy.</param>
/// <param name="watermark">Optional text to insert in the middle of the entropy string for traceability.</param>
/// <param name="delimiter">The delimiter character used to separate parts of the ID.</param>
/// <param name="partitionKey">An explicit partition key to use. When provided, this value will be used instead of generating a new one.</param>
/// <param name="partitionKeyHint">An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.</param>
/// <param name="random">The random number generator.</param>
/// <returns>A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".</returns>
/// <exception cref="ArgumentException">Thrown when the watermark contains non-alphanumeric characters.</exception>
public static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "",
Random? random = null)
{
ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
var entropy = GetRandomString(stringLength, random);
string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength, random);
if (!string.IsNullOrEmpty(watermark))
{
if (!WatermarkRegex().IsMatch(watermark))
{
throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
nameof(watermark));
}
entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
}
infix ??= "";
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
return $"{prefix}{infix}{entropy}{pKey}";
}
/// <summary>
/// Generates a secure random alphanumeric string of the specified length.
/// When a random seed was provided to the constructor, uses deterministic generation.
/// </summary>
/// <param name="stringLength">The desired length of the random string.</param>
/// <param name="random">The optional random number generator.</param>
/// <returns>A random alphanumeric string.</returns>
/// <exception cref="ArgumentException">Thrown when stringLength is less than 1.</exception>
private static string GetRandomString(int stringLength, Random? random)
{
const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
if (random is not null)
{
// Use deterministic random generation when seed is provided
return string.Create(stringLength, random, static (destination, random) =>
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = Chars[random.Next(Chars.Length)];
}
});
}
// Use cryptographically secure random generation when no seed is provided
return RandomNumberGenerator.GetString(Chars, stringLength);
}
/// <summary>
/// Extracts the partition key from an existing ID, or returns null if extraction fails.
/// </summary>
/// <param name="id">The ID to extract the partition key from.</param>
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
/// <param name="delimiter">The delimiter character used in the ID.</param>
/// <returns>The partition key if successfully extracted; otherwise, null.</returns>
private static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
string delimiter = "_")
{
if (string.IsNullOrEmpty(id))
{
return null;
}
var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
return null;
}
if (parts[1].Length < stringLength + partitionKeyLength)
{
return null;
}
// get last partitionKeyLength characters from the last part as the partition key
return parts[1][^partitionKeyLength..];
}
}
@@ -1,98 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// Shared helpers to generate IDs.
/// </summary>
internal static partial class IdGeneratorHelpers
{
#if NET9_0_OR_GREATER
[GeneratedRegex("^[A-Za-z0-9]+$")]
private static partial Regex WatermarkRegex();
#else
private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
private static Regex WatermarkRegex() => s_watermarkRegex;
#endif
/// <summary>
/// Generates a new ID with a structured format that includes a partition key.
/// </summary>
/// <param name="prefix">The prefix to add to the ID, typically indicating the resource type.</param>
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
/// <param name="infix">Optional additional text to insert between the prefix and the entropy.</param>
/// <param name="watermark">Optional text to insert in the middle of the entropy string for traceability.</param>
/// <param name="delimiter">The delimiter character used to separate parts of the ID.</param>
/// <param name="partitionKey">An explicit partition key to use. When provided, this value will be used instead of generating a new one.</param>
/// <param name="partitionKeyHint">An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.</param>
/// <returns>A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".</returns>
/// <exception cref="ArgumentException">Thrown when the watermark contains non-alphanumeric characters.</exception>
public static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "")
{
ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
var entropy = GetRandomString(stringLength);
string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength);
if (!string.IsNullOrEmpty(watermark))
{
if (!WatermarkRegex().IsMatch(watermark))
{
throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
nameof(watermark));
}
entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
}
infix ??= "";
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
return $"{prefix}{infix}{entropy}{pKey}";
}
/// <summary>
/// Generates a secure random alphanumeric string of the specified length.
/// </summary>
/// <param name="stringLength">The desired length of the random string.</param>
/// <returns>A random alphanumeric string.</returns>
/// <exception cref="ArgumentException">Thrown when stringLength is less than 1.</exception>
public static string GetRandomString(int stringLength) =>
RandomNumberGenerator.GetString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", stringLength);
/// <summary>
/// Extracts the partition key from an existing ID, or returns null if extraction fails.
/// </summary>
/// <param name="id">The ID to extract the partition key from.</param>
/// <param name="stringLength">The length of the random entropy string in the ID.</param>
/// <param name="partitionKeyLength">The length of the partition key if generating a new one.</param>
/// <param name="delimiter">The delimiter character used in the ID.</param>
/// <returns>The partition key if successfully extracted; otherwise, null.</returns>
public static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
string delimiter = "_")
{
if (string.IsNullOrEmpty(id))
{
return null;
}
var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
return null;
}
if (parts[1].Length < stringLength + partitionKeyLength)
{
return null;
}
// get last partitionKeyLength characters from the last part as the partition key
return parts[1][^partitionKeyLength..];
}
}
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// Configuration options for in-memory storage implementations.
/// </summary>
internal sealed class InMemoryStorageOptions
{
/// <summary>
/// Gets or sets the maximum number of items to store in the cache.
/// Default is 1000. Set to null for no size limit.
/// </summary>
public long? SizeLimit { get; set; } = 1000;
/// <summary>
/// Gets or sets the absolute expiration time for items in storage.
/// If specified, items will be expired after this timespan regardless of access.
/// Default is null (no absolute expiration).
/// </summary>
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
/// <summary>
/// Gets or sets the sliding expiration for items in storage.
/// Items will be expired if not accessed within this timespan.
/// Default is 1 hour.
/// </summary>
public TimeSpan? SlidingExpiration { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Creates <see cref="MemoryCacheOptions"/> from these options.
/// </summary>
internal MemoryCacheOptions ToMemoryCacheOptions() => new()
{
SizeLimit = this.SizeLimit
};
/// <summary>
/// Creates <see cref="MemoryCacheEntryOptions"/> from these options.
/// </summary>
internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions() => new()
{
AbsoluteExpirationRelativeToNow = this.AbsoluteExpirationRelativeToNow,
SlidingExpiration = this.SlidingExpiration,
Size = 1
};
}
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// Extension methods for <see cref="IMemoryCache"/> that provide atomic operations.
/// </summary>
/// <remarks>
/// The standard GetOrCreate method has a race condition where multiple threads can simultaneously
/// detect that a key doesn't exist and create different instances, with only one being cached.
/// See: https://github.com/dotnet/runtime/issues/36499
/// </remarks>
internal static class MemoryCacheExtensions
{
private static readonly ConcurrentDictionary<(IMemoryCache, object), SemaphoreSlim> s_semaphores = new();
/// <summary>
/// Atomically gets the value associated with this key if it exists, or generates a new entry
/// using the provided key and a value from the given factory if the key is not found.
/// </summary>
/// <typeparam name="T">The type of the object to get.</typeparam>
/// <param name="memoryCache">The <see cref="IMemoryCache"/> instance this method extends.</param>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">The factory that creates the value associated with this key if the key does not exist in the cache.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A tuple containing the value and a flag indicating whether it was created (true) or retrieved from cache (false).</returns>
public static async Task<T> GetOrCreateAtomicAsync<T>(
this IMemoryCache memoryCache,
object key,
Func<ICacheEntry, T> factory,
CancellationToken cancellationToken = default)
{
// Fast path: check if the value already exists
if (memoryCache.TryGetValue(key, out object? value))
{
Debug.Assert(value is not null);
return (T)value;
}
// Get or create a semaphore for this cache key
bool isOwner = false;
var semaphoreKey = (memoryCache, key);
if (!s_semaphores.TryGetValue(semaphoreKey, out SemaphoreSlim? semaphore))
{
SemaphoreSlim? createdSemaphore = null;
semaphore = s_semaphores.GetOrAdd(semaphoreKey, _ => createdSemaphore = new SemaphoreSlim(1));
// If we created the semaphore that made it into the dictionary, we're the owner
if (ReferenceEquals(createdSemaphore, semaphore))
{
isOwner = true;
}
else
{
// Our semaphore wasn't the one stored, so dispose it
createdSemaphore?.Dispose();
}
}
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Double-check: another thread might have created the value while we were waiting
if (!memoryCache.TryGetValue(key, out value))
{
ICacheEntry entry = memoryCache.CreateEntry(key);
entry.SetValue(value = factory(entry));
entry.Dispose();
Debug.Assert(value is not null);
return (T)value;
}
Debug.Assert(value is not null);
return (T)value;
}
finally
{
// If we were the owner of the semaphore, remove it from the dictionary
// This prevents memory leaks from accumulating semaphores for evicted cache entries
if (isOwner)
{
s_semaphores.TryRemove(semaphoreKey, out _);
}
semaphore.Release();
}
}
}
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugCoreTargetFrameworks)</TargetFrameworks>
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
<NoWarn>$(NoWarn);OPENAI001;MEAI001</NoWarn>
<RootNamespace>Microsoft.Agents.AI.Hosting.OpenAI</RootNamespace>
<VersionSuffix>alpha</VersionSuffix>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsNamespaces>
@@ -25,6 +25,7 @@
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Models;
/// <summary>
/// Response for a delete operation.
/// </summary>
internal sealed class DeleteResponse
{
/// <summary>
/// The ID of the deleted object.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// The object type.
/// </summary>
[JsonPropertyName("object")]
[SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")]
public required string Object { get; init; }
/// <summary>
/// Whether the object was successfully deleted.
/// </summary>
[JsonPropertyName("deleted")]
public required bool Deleted { get; init; }
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Models;
/// <summary>
/// Represents an error response from the OpenAI APIs.
/// </summary>
internal sealed class ErrorResponse
{
/// <summary>
/// Gets the error details.
/// </summary>
[JsonPropertyName("error")]
public required ErrorDetails Error { get; init; }
}
/// <summary>
/// Represents the details of an error.
/// </summary>
internal sealed class ErrorDetails
{
/// <summary>
/// Gets the error message.
/// </summary>
[JsonPropertyName("message")]
public required string Message { get; init; }
/// <summary>
/// Gets the error type.
/// </summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>
/// Gets the error code.
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; init; }
/// <summary>
/// Gets the parameter that caused the error.
/// </summary>
[JsonPropertyName("param")]
public string? Param { get; init; }
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Models;
/// <summary>
/// Generic list response for paginated results.
/// Used across the OpenAI API for listing resources.
/// </summary>
internal sealed class ListResponse<T>
{
/// <summary>
/// The object type, always "list".
/// </summary>
[JsonPropertyName("object")]
[SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")]
public string Object => "list";
/// <summary>
/// The list of items.
/// </summary>
[JsonPropertyName("data")]
public required List<T> Data { get; init; }
/// <summary>
/// The ID of the first item in the list.
/// </summary>
[JsonPropertyName("first_id")]
public string? FirstId { get; init; }
/// <summary>
/// The ID of the last item in the list.
/// </summary>
[JsonPropertyName("last_id")]
public string? LastId { get; init; }
/// <summary>
/// Whether there are more items available.
/// </summary>
[JsonPropertyName("has_more")]
public required bool HasMore { get; init; }
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Models;
/// <summary>
/// Specifies the sort order for list operations.
/// </summary>
[JsonConverter(typeof(SortOrderJsonConverter))]
internal enum SortOrder
{
/// <summary>
/// Sort in ascending order (oldest to newest).
/// </summary>
Ascending,
/// <summary>
/// Sort in descending order (newest to oldest).
/// </summary>
Descending
}
/// <summary>
/// Custom JSON converter for SortOrder enum to serialize as "asc" and "desc".
/// </summary>
internal sealed class SortOrderJsonConverter : JsonConverter<SortOrder>
{
/// <inheritdoc/>
public override SortOrder Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return value switch
{
string s when s.Equals("asc", StringComparison.OrdinalIgnoreCase) => SortOrder.Ascending,
string s when s.Equals("desc", StringComparison.OrdinalIgnoreCase) => SortOrder.Descending,
null => throw new JsonException("SortOrder value cannot be null"),
_ => throw new JsonException($"Invalid SortOrder value: {value}")
};
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, SortOrder value, JsonSerializerOptions options)
{
var stringValue = value switch
{
SortOrder.Ascending => "asc",
SortOrder.Descending => "desc",
_ => throw new JsonException($"Invalid SortOrder value: {value}")
};
writer.WriteStringValue(stringValue);
}
}
@@ -4,16 +4,54 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
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.Responses;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// Provides JSON serialization options and context for OpenAI Hosting APIs to support AOT and trimming.
/// </summary>
internal static class OpenAIHostingJsonUtilities
{
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for OpenAI API serialization.
/// Includes support for AIContent types and all OpenAI-related types.
/// </summary>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
private static JsonSerializerOptions CreateDefaultOptions()
{
JsonSerializerOptions options = new(OpenAIHostingJsonContext.Default.Options);
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.MakeReadOnly();
return options;
}
}
/// <summary>
/// Provides a unified JSON serialization context for all OpenAI Hosting APIs to support AOT and trimming.
/// Combines Conversations and Responses API types.
/// </summary>
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
AllowOutOfOrderMetadataProperties = true,
WriteIndented = false)]
[JsonSerializable(typeof(Dictionary<string, string>))]
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
AllowOutOfOrderMetadataProperties = true,
WriteIndented = false)]
// Conversations API types
[JsonSerializable(typeof(Conversation))]
[JsonSerializable(typeof(ListResponse<Conversation>))]
[JsonSerializable(typeof(CreateConversationRequest))]
[JsonSerializable(typeof(CreateItemsRequest))]
[JsonSerializable(typeof(UpdateConversationRequest))]
[JsonSerializable(typeof(ListResponse<ItemResource>))]
[JsonSerializable(typeof(List<Conversation>))]
// Shared types
[JsonSerializable(typeof(DeleteResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(ErrorDetails))]
// Responses API types
[JsonSerializable(typeof(CreateResponse))]
[JsonSerializable(typeof(Response))]
[JsonSerializable(typeof(StreamingResponseEvent))]
@@ -40,11 +78,9 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
[JsonSerializable(typeof(ResponseInput))]
[JsonSerializable(typeof(InputMessage))]
[JsonSerializable(typeof(List<InputMessage>))]
[JsonSerializable(typeof(IReadOnlyList<InputMessage>))]
[JsonSerializable(typeof(InputMessageContent))]
[JsonSerializable(typeof(ResponseStatus))]
[JsonSerializable(typeof(List<ItemContent>))]
[JsonSerializable(typeof(IList<ItemContent>))]
// ItemResource types
[JsonSerializable(typeof(ItemResource))]
[JsonSerializable(typeof(ResponsesMessageItemResource))]
[JsonSerializable(typeof(ResponsesAssistantMessageItemResource))]
@@ -67,8 +103,35 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
[JsonSerializable(typeof(MCPApprovalRequestItemResource))]
[JsonSerializable(typeof(MCPApprovalResponseItemResource))]
[JsonSerializable(typeof(MCPCallItemResource))]
[JsonSerializable(typeof(IList<ItemResource>))]
[JsonSerializable(typeof(List<ItemResource>))]
// ItemParam types
[JsonSerializable(typeof(ItemParam))]
[JsonSerializable(typeof(ResponsesMessageItemParam))]
[JsonSerializable(typeof(ResponsesUserMessageItemParam))]
[JsonSerializable(typeof(ResponsesAssistantMessageItemParam))]
[JsonSerializable(typeof(ResponsesSystemMessageItemParam))]
[JsonSerializable(typeof(ResponsesDeveloperMessageItemParam))]
[JsonSerializable(typeof(FunctionToolCallItemParam))]
[JsonSerializable(typeof(FunctionToolCallOutputItemParam))]
[JsonSerializable(typeof(FileSearchToolCallItemParam))]
[JsonSerializable(typeof(ComputerToolCallItemParam))]
[JsonSerializable(typeof(ComputerToolCallOutputItemParam))]
[JsonSerializable(typeof(WebSearchToolCallItemParam))]
[JsonSerializable(typeof(ReasoningItemParam))]
[JsonSerializable(typeof(ItemReferenceItemParam))]
[JsonSerializable(typeof(ImageGenerationToolCallItemParam))]
[JsonSerializable(typeof(CodeInterpreterToolCallItemParam))]
[JsonSerializable(typeof(LocalShellToolCallItemParam))]
[JsonSerializable(typeof(LocalShellToolCallOutputItemParam))]
[JsonSerializable(typeof(MCPListToolsItemParam))]
[JsonSerializable(typeof(MCPApprovalRequestItemParam))]
[JsonSerializable(typeof(MCPApprovalResponseItemParam))]
[JsonSerializable(typeof(MCPCallItemParam))]
[JsonSerializable(typeof(List<ItemParam>))]
// ItemContent types
[JsonSerializable(typeof(List<ItemContent>))]
[JsonSerializable(typeof(IReadOnlyList<ItemContent>))]
[JsonSerializable(typeof(ItemContent[]))]
[JsonSerializable(typeof(ItemContent))]
[JsonSerializable(typeof(ItemContentInputText))]
[JsonSerializable(typeof(ItemContentInputAudio))]
@@ -82,5 +145,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
[JsonSerializable(typeof(ResponseTextFormatConfigurationText))]
[JsonSerializable(typeof(ResponseTextFormatConfigurationJsonObject))]
[JsonSerializable(typeof(ResponseTextFormatConfigurationJsonSchema))]
// Common types
[JsonSerializable(typeof(Dictionary<string, string>))]
[ExcludeFromCodeCoverage]
internal sealed partial class ResponsesJsonContext : JsonSerializerContext;
internal sealed partial class OpenAIHostingJsonContext : JsonSerializerContext;
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Response executor that uses an AIAgent to execute responses locally.
/// This is the default implementation for local execution.
/// </summary>
internal sealed class AIAgentResponseExecutor : IResponseExecutor
{
private readonly AIAgent _agent;
public AIAgentResponseExecutor(AIAgent agent)
{
ArgumentNullException.ThrowIfNull(agent);
this._agent = agent;
}
public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
AgentInvocationContext context,
CreateResponse request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create options with properties from the request
var chatOptions = new ChatOptions
{
ConversationId = request.Conversation?.Id,
Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
Instructions = request.Instructions,
ModelId = request.Model,
};
var options = new ChatClientAgentRunOptions(chatOptions);
// Convert input to chat messages
var messages = new List<ChatMessage>();
foreach (var inputMessage in request.Input.GetInputMessages())
{
messages.Add(inputMessage.ToChatMessage());
}
// Use the extension method to convert streaming updates to streaming response events
await foreach (var streamingEvent in this._agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken)
.ToStreamingResponseAsync(request, context, cancellationToken)
.ConfigureAwait(false))
{
yield return streamingEvent;
}
}
}
@@ -1,65 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Net.ServerSentEvents;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// OpenAI Responses processor for <see cref="AIAgent"/>.
/// </summary>
internal static class AIAgentResponsesProcessor
{
public static async Task<IResult> CreateModelResponseAsync(AIAgent agent, CreateResponse request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(agent);
var context = new AgentInvocationContext(idGenerator: IdGenerator.From(request));
if (request.Stream == true)
{
return new StreamingResponse(agent, request, context);
}
var messages = request.Input.GetInputMessages().Select(i => i.ToChatMessage());
var response = await agent.RunAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false);
return Results.Ok(response.ToResponse(request, context));
}
private sealed class StreamingResponse(AIAgent agent, CreateResponse createResponse, AgentInvocationContext context) : IResult
{
public Task ExecuteAsync(HttpContext httpContext)
{
var cancellationToken = httpContext.RequestAborted;
var response = httpContext.Response;
// Set SSE headers
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache,no-store";
response.Headers.Connection = "keep-alive";
response.Headers.ContentEncoding = "identity";
httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();
var chatMessages = createResponse.Input.GetInputMessages().Select(i => i.ToChatMessage()).ToList();
var events = agent.RunStreamingAsync(chatMessages, cancellationToken: cancellationToken)
.ToStreamingResponseAsync(createResponse, context, cancellationToken)
.Select(static evt => new SseItem<StreamingResponseEvent>(evt, evt.Type));
return SseFormatter.WriteAsync(
source: events,
destination: response.Body,
itemFormatter: static (sseItem, bufferWriter) =>
{
using var writer = new Utf8JsonWriter(bufferWriter);
JsonSerializer.Serialize(writer, sseItem.Data, ResponsesJsonContext.Default.StreamingResponseEvent);
writer.Flush();
},
cancellationToken);
}
}
}
@@ -29,5 +29,5 @@ internal sealed class AgentInvocationContext(IdGenerator idGenerator, JsonSerial
/// <summary>
/// Gets the JSON serializer options.
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? ResponsesJsonSerializerOptions.Default;
public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? OpenAIHostingJsonUtilities.DefaultOptions;
}
@@ -15,6 +15,8 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// </summary>
internal static class AgentRunResponseExtensions
{
private static ChatRole s_DeveloperRole => new("developer");
/// <summary>
/// Converts an AgentRunResponse to a Response model.
/// </summary>
@@ -44,38 +46,38 @@ internal static class AgentRunResponseExtensions
return new Response
{
Id = context.ResponseId,
CreatedAt = (agentRunResponse.CreatedAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
Model = request.Agent?.Name ?? request.Model,
Status = ResponseStatus.Completed,
Agent = request.Agent?.ToAgentId(),
Background = request.Background,
Conversation = request.Conversation ?? (context.ConversationId != null ? new ConversationReference { Id = context.ConversationId } : null),
Metadata = request.Metadata is IReadOnlyDictionary<string, string> metadata ? new Dictionary<string, string>(metadata) : [],
CreatedAt = (agentRunResponse.CreatedAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
Error = null,
Id = context.ResponseId,
Instructions = request.Instructions,
Temperature = request.Temperature ?? 1.0,
TopP = request.TopP ?? 1.0,
Output = output,
Usage = agentRunResponse.Usage.ToResponseUsage(),
ParallelToolCalls = request.ParallelToolCalls ?? true,
Tools = [.. request.Tools ?? []],
ToolChoice = request.ToolChoice,
ServiceTier = request.ServiceTier ?? "default",
Store = request.Store ?? true,
PreviousResponseId = request.PreviousResponseId,
Reasoning = request.Reasoning,
Text = request.Text,
MaxOutputTokens = request.MaxOutputTokens,
MaxToolCalls = request.MaxToolCalls,
Metadata = request.Metadata is IReadOnlyDictionary<string, string> metadata ? new Dictionary<string, string>(metadata) : [],
Model = request.Agent?.Name ?? request.Model,
Output = output,
ParallelToolCalls = request.ParallelToolCalls ?? true,
PreviousResponseId = request.PreviousResponseId,
Prompt = request.Prompt,
PromptCacheKey = request.PromptCacheKey,
Reasoning = request.Reasoning,
SafetyIdentifier = request.SafetyIdentifier,
ServiceTier = request.ServiceTier ?? "default",
Status = ResponseStatus.Completed,
Store = request.Store ?? true,
Temperature = request.Temperature ?? 1.0,
Text = request.Text,
ToolChoice = request.ToolChoice,
Tools = [.. request.Tools ?? []],
TopLogprobs = request.TopLogprobs,
TopP = request.TopP ?? 1.0,
Truncation = request.Truncation,
Usage = agentRunResponse.Usage.ToResponseUsage(),
#pragma warning disable CS0618 // Type or member is obsolete
User = request.User,
#pragma warning restore CS0618 // Type or member is obsolete
PromptCacheKey = request.PromptCacheKey,
SafetyIdentifier = request.SafetyIdentifier,
TopLogprobs = request.TopLogprobs,
MaxToolCalls = request.MaxToolCalls,
Background = request.Background,
Prompt = request.Prompt,
Error = null
};
}
@@ -88,22 +90,19 @@ internal static class AgentRunResponseExtensions
/// <returns>An enumerable of ItemResource objects.</returns>
public static IEnumerable<ItemResource> ToItemResource(this ChatMessage message, IdGenerator idGenerator, JsonSerializerOptions jsonSerializerOptions)
{
IList<ItemContent> contents = [];
foreach (var content in message.Contents)
List<ItemContent> contents = [];
foreach (AIContent content in message.Contents)
{
switch (content)
{
case FunctionCallContent functionCallContent:
// message.Role == ChatRole.Assistant
yield return functionCallContent.ToFunctionToolCallItemResource(idGenerator.GenerateFunctionCallId(), jsonSerializerOptions);
break;
case FunctionResultContent functionResultContent:
// message.Role == ChatRole.Tool
yield return functionResultContent.ToFunctionToolCallOutputItemResource(
idGenerator.GenerateFunctionOutputId());
break;
default:
// message.Role == ChatRole.Assistant
if (ItemContentConverter.ToItemContent(content) is { } itemContent)
{
contents.Add(itemContent);
@@ -115,12 +114,34 @@ internal static class AgentRunResponseExtensions
if (contents.Count > 0)
{
yield return new ResponsesAssistantMessageItemResource
{
Id = idGenerator.GenerateMessageId(),
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contents
};
List<ItemContent> contentArray = contents;
string messageId = idGenerator.GenerateMessageId();
yield return
message.Role == ChatRole.User ? new ResponsesUserMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
message.Role == ChatRole.System ? new ResponsesSystemMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
message.Role == s_DeveloperRole ? new ResponsesDeveloperMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
new ResponsesAssistantMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
};
}
}
@@ -168,6 +189,49 @@ internal static class AgentRunResponseExtensions
};
}
/// <summary>
/// Converts an InputMessage to ItemResource objects.
/// </summary>
/// <param name="inputMessage">The input message to convert.</param>
/// <param name="idGenerator">The ID generator to use for creating IDs.</param>
/// <returns>An enumerable of ItemResource objects.</returns>
public static IEnumerable<ItemResource> ToItemResource(this InputMessage inputMessage, IdGenerator idGenerator)
{
// Convert InputMessageContent to ItemContent array
List<ItemContent> contentArray = inputMessage.Content.ToItemContents();
// Generate a message ID
string messageId = idGenerator.GenerateMessageId();
// Create the appropriate message type based on role
ChatRole role = new(inputMessage.Role.Value);
yield return
role == ChatRole.User ? new ResponsesUserMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
role == ChatRole.System ? new ResponsesSystemMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
role == s_DeveloperRole ? new ResponsesDeveloperMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
} :
new ResponsesAssistantMessageItemResource
{
Id = messageId,
Status = ResponsesMessageItemResourceStatus.Completed,
Content = contentArray
};
}
/// <summary>
/// Converts UsageDetails to ResponseUsage.
/// </summary>
@@ -4,11 +4,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
@@ -26,7 +28,7 @@ internal static class AgentRunResponseUpdateExtensions
/// <param name="context">The agent invocation context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A stream of response events.</returns>
internal static async IAsyncEnumerable<StreamingResponseEvent> ToStreamingResponseAsync(
public static async IAsyncEnumerable<StreamingResponseEvent> ToStreamingResponseAsync(
this IAsyncEnumerable<AgentRunResponseUpdate> updates,
CreateResponse request,
AgentInvocationContext context,
@@ -50,6 +52,13 @@ internal static class AgentRunResponseUpdateExtensions
cancellationToken.ThrowIfCancellationRequested();
var update = updateEnumerator.Current;
// Special-case for agent framework workflow events.
if (update.RawRepresentation is WorkflowEvent workflowEvent)
{
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
continue;
}
if (!IsSameMessage(update, previousUpdate))
{
// Finalize the current generator when moving to a new message.
@@ -99,6 +108,8 @@ internal static class AgentRunResponseUpdateExtensions
TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionApprovalRequestContent => new FunctionApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionApprovalResponseContent => new FunctionApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex),
ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex),
UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
@@ -144,38 +155,38 @@ internal static class AgentRunResponseUpdateExtensions
{
return new Response
{
Id = context.ResponseId,
CreatedAt = createdAt.ToUnixTimeSeconds(),
Model = request.Agent?.Name ?? request.Model,
Status = status,
Agent = request.Agent?.ToAgentId(),
Background = request.Background,
Conversation = request.Conversation ?? new ConversationReference { Id = context.ConversationId },
Metadata = request.Metadata != null ? new Dictionary<string, string>(request.Metadata) : [],
CreatedAt = createdAt.ToUnixTimeSeconds(),
Error = null,
Id = context.ResponseId,
Instructions = request.Instructions,
Temperature = request.Temperature ?? 1.0,
TopP = request.TopP ?? 1.0,
Output = outputs?.ToList() ?? [],
Usage = latestUsage,
ParallelToolCalls = request.ParallelToolCalls ?? true,
Tools = [.. request.Tools ?? []],
ToolChoice = request.ToolChoice,
ServiceTier = request.ServiceTier ?? "default",
Store = request.Store ?? true,
PreviousResponseId = request.PreviousResponseId,
Reasoning = request.Reasoning,
Text = request.Text,
MaxOutputTokens = request.MaxOutputTokens,
MaxToolCalls = request.MaxToolCalls,
Metadata = request.Metadata != null ? new Dictionary<string, string>(request.Metadata) : [],
Model = request.Agent?.Name ?? request.Model,
Output = outputs?.ToList() ?? [],
ParallelToolCalls = request.ParallelToolCalls ?? true,
PreviousResponseId = request.PreviousResponseId,
Prompt = request.Prompt,
PromptCacheKey = request.PromptCacheKey,
Reasoning = request.Reasoning,
SafetyIdentifier = request.SafetyIdentifier,
ServiceTier = request.ServiceTier,
Status = status,
Store = request.Store ?? true,
Temperature = request.Temperature ?? 1.0,
Text = request.Text,
ToolChoice = request.ToolChoice,
Tools = [.. request.Tools ?? []],
TopLogprobs = request.TopLogprobs,
TopP = request.TopP ?? 1.0,
Truncation = request.Truncation,
Usage = latestUsage,
#pragma warning disable CS0618 // Type or member is obsolete
User = request.User,
PromptCacheKey = request.PromptCacheKey,
#pragma warning restore CS0618 // Type or member is obsolete
SafetyIdentifier = request.SafetyIdentifier,
TopLogprobs = request.TopLogprobs,
MaxToolCalls = request.MaxToolCalls,
Background = request.Background,
Prompt = request.Prompt,
Error = null
};
}
}
@@ -192,4 +203,49 @@ internal static class AgentRunResponseUpdateExtensions
static bool IsSameRole(ChatRole? value1, ChatRole? value2) =>
!value1.HasValue || !value2.HasValue || value1.Value == value2.Value;
}
private static StreamingWorkflowEventComplete CreateWorkflowEventResponse(WorkflowEvent workflowEvent, int sequenceNumber, int outputIndex)
{
// Extract executor_id if this is an ExecutorEvent
string? executorId = null;
if (workflowEvent is ExecutorEvent execEvent)
{
executorId = execEvent.ExecutorId;
}
JsonElement eventData;
if (JsonSerializer.IsReflectionEnabledByDefault)
{
JsonElement? dataElement = null;
if (workflowEvent.Data is not null)
{
dataElement = JsonSerializer.SerializeToElement(workflowEvent.Data, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
var eventDataObj = new WorkflowEventData
{
EventType = workflowEvent.GetType().Name,
Data = dataElement,
ExecutorId = executorId,
Timestamp = DateTime.UtcNow.ToString("O")
};
eventData = JsonSerializer.SerializeToElement(eventDataObj, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(WorkflowEventData)));
}
else
{
eventData = JsonSerializer.SerializeToElement(
"Unsupported. Workflow event serialization is currently only supported when JsonSerializer.IsReflectionEnabledByDefault is true.",
OpenAIHostingJsonContext.Default.String);
}
// Create the properly typed streaming workflow event
return new StreamingWorkflowEventComplete
{
SequenceNumber = sequenceNumber,
OutputIndex = outputIndex,
Data = eventData,
ExecutorId = executorId,
ItemId = IdGenerator.NewId(prefix: "wf", stringLength: 8, delimiter: "")
};
}
}
@@ -11,6 +11,23 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
/// </summary>
internal static class ItemContentConverter
{
private static string AudioFormatToMediaType(string? format) =>
format?.Equals("mp3", StringComparison.OrdinalIgnoreCase) == true ? "audio/mpeg" :
format?.Equals("wav", StringComparison.OrdinalIgnoreCase) == true ? "audio/wav" :
format?.Equals("opus", StringComparison.OrdinalIgnoreCase) == true ? "audio/opus" :
format?.Equals("aac", StringComparison.OrdinalIgnoreCase) == true ? "audio/aac" :
format?.Equals("flac", StringComparison.OrdinalIgnoreCase) == true ? "audio/flac" :
format?.Equals("pcm16", StringComparison.OrdinalIgnoreCase) == true ? "audio/pcm" :
"audio/*";
private static string MediaTypeToAudioFormat(string mediaType) =>
mediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase) ? "mp3" :
mediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase) ? "wav" :
mediaType.Equals("audio/opus", StringComparison.OrdinalIgnoreCase) ? "opus" :
mediaType.Equals("audio/aac", StringComparison.OrdinalIgnoreCase) ? "aac" :
mediaType.Equals("audio/flac", StringComparison.OrdinalIgnoreCase) ? "flac" :
mediaType.Equals("audio/pcm", StringComparison.OrdinalIgnoreCase) ? "pcm16" :
"mp3";
/// <summary>
/// Converts <see cref="ItemContent"/> to <see cref="AIContent"/>.
/// </summary>
@@ -49,16 +66,7 @@ internal static class ItemContentConverter
// Audio content - map to DataContent with media type based on format
ItemContentInputAudio inputAudio =>
new DataContent(inputAudio.Data, inputAudio.Format?.ToUpperInvariant() switch
{
"MP3" => "audio/mpeg",
"WAV" => "audio/wav",
"OPUS" => "audio/opus",
"AAC" => "audio/aac",
"FLAC" => "audio/flac",
"PCM16" => "audio/pcm",
_ => "audio/*"
}),
new DataContent(inputAudio.Data, AudioFormatToMediaType(inputAudio.Format)),
ItemContentOutputAudio outputAudio =>
new DataContent(outputAudio.Data, "audio/*"),
@@ -104,34 +112,28 @@ internal static class ItemContentConverter
ImageUrl = uriContent.Uri?.ToString(),
Detail = GetImageDetail(uriContent)
},
HostedFileContent hostedFile =>
new ItemContentInputFile
{
FileId = hostedFile.FileId
},
DataContent dataContent when dataContent.HasTopLevelMediaType("image") =>
new ItemContentInputImage
{
ImageUrl = dataContent.Uri,
Detail = GetImageDetail(dataContent)
},
HostedFileContent hostedFile =>
new ItemContentInputFile
{
FileId = hostedFile.FileId
},
DataContent fileData when !fileData.HasTopLevelMediaType("image") && !fileData.HasTopLevelMediaType("audio") =>
new ItemContentInputFile
{
FileData = fileData.Uri,
Filename = fileData.Name
},
DataContent audioData when audioData.HasTopLevelMediaType("audio") =>
new ItemContentInputAudio
{
Data = audioData.Uri,
Format = audioData.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase) ? "mp3" :
audioData.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase) ? "wav" :
audioData.MediaType.Equals("audio/opus", StringComparison.OrdinalIgnoreCase) ? "opus" :
audioData.MediaType.Equals("audio/aac", StringComparison.OrdinalIgnoreCase) ? "aac" :
audioData.MediaType.Equals("audio/flac", StringComparison.OrdinalIgnoreCase) ? "flac" :
audioData.MediaType.Equals("audio/pcm", StringComparison.OrdinalIgnoreCase) ? "pcm16" :
"mp3" // Default to mp3
Format = MediaTypeToAudioFormat(audioData.MediaType)
},
DataContent fileData =>
new ItemContentInputFile
{
FileData = fileData.Uri,
Filename = fileData.Name
},
// Other AIContent types (FunctionCallContent, FunctionResultContent, etc.)
// are handled separately in the Responses API as different ItemResource types, not ItemContent
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
/// <summary>
/// JSON converter for ItemParam that handles polymorphic deserialization based on the "type" discriminator.
/// </summary>
internal sealed class ItemParamConverter : JsonConverter<ItemParam>
{
public override ItemParam? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
if (!root.TryGetProperty("type", out var typeElement))
{
throw new JsonException("ItemParam must have a 'type' property");
}
var type = typeElement.GetString();
// Use OpenAIJsonContext directly since it has all the ItemParam type metadata
return type switch
{
"message" => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesMessageItemParam),
"function_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.FunctionToolCallItemParam),
"function_call_output" => doc.Deserialize(OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemParam),
"file_search_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.FileSearchToolCallItemParam),
"computer_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.ComputerToolCallItemParam),
"computer_call_output" => doc.Deserialize(OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemParam),
"web_search_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.WebSearchToolCallItemParam),
"reasoning" => doc.Deserialize(OpenAIHostingJsonContext.Default.ReasoningItemParam),
"item_reference" => doc.Deserialize(OpenAIHostingJsonContext.Default.ItemReferenceItemParam),
"image_generation_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemParam),
"code_interpreter_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemParam),
"local_shell_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.LocalShellToolCallItemParam),
"local_shell_call_output" => doc.Deserialize(OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemParam),
"mcp_list_tools" => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPListToolsItemParam),
"mcp_approval_request" => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalRequestItemParam),
"mcp_approval_response" => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalResponseItemParam),
"mcp_call" => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPCallItemParam),
_ => null // Ignore unknown types.
};
}
public override void Write(Utf8JsonWriter writer, ItemParam value, JsonSerializerOptions options)
{
// Use OpenAIJsonContext directly to serialize the concrete type
JsonSerializer.Serialize(writer, value, OpenAIHostingJsonContext.Default.Options.GetTypeInfo(value.GetType()));
}
}
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
@@ -11,137 +10,101 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
/// <summary>
/// JSON converter for ItemResource that handles type discrimination.
/// </summary>
[ExcludeFromCodeCoverage]
internal sealed class ItemResourceConverter : JsonConverter<ItemResource>
{
private readonly ResponsesJsonContext _context;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResourceConverter"/> class.
/// </summary>
public ItemResourceConverter()
{
this._context = ResponsesJsonContext.Default;
}
/// <inheritdoc/>
public override ItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Clone the reader to peek at the JSON
Utf8JsonReader readerClone = reader;
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
// Read through the JSON to find the type property
string? type = null;
if (readerClone.TokenType != JsonTokenType.StartObject)
if (!root.TryGetProperty("type", out var typeElement))
{
throw new JsonException("Expected start of object");
throw new JsonException("ItemResource must have a 'type' property");
}
while (readerClone.Read())
{
if (readerClone.TokenType == JsonTokenType.EndObject)
{
break;
}
if (readerClone.TokenType == JsonTokenType.PropertyName)
{
string propertyName = readerClone.GetString()!;
readerClone.Read(); // Move to the value
if (propertyName == "type")
{
type = readerClone.GetString();
break;
}
if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
{
// Skip nested objects/arrays
readerClone.Skip();
}
}
}
var type = typeElement.GetString();
// Determine the concrete type based on the type discriminator and deserialize using the source generation context
return type switch
{
ResponsesMessageItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesMessageItemResource),
FileSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FileSearchToolCallItemResource),
FunctionToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallItemResource),
FunctionToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallOutputItemResource),
ComputerToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallItemResource),
ComputerToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallOutputItemResource),
WebSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.WebSearchToolCallItemResource),
ReasoningItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ReasoningItemResource),
ItemReferenceItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ItemReferenceItemResource),
ImageGenerationToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ImageGenerationToolCallItemResource),
CodeInterpreterToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.CodeInterpreterToolCallItemResource),
LocalShellToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallItemResource),
LocalShellToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallOutputItemResource),
MCPListToolsItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPListToolsItemResource),
MCPApprovalRequestItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalRequestItemResource),
MCPApprovalResponseItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalResponseItemResource),
MCPCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPCallItemResource),
_ => throw new JsonException($"Unknown item type: {type}")
ResponsesMessageItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesMessageItemResource),
FileSearchToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.FileSearchToolCallItemResource),
FunctionToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.FunctionToolCallItemResource),
FunctionToolCallOutputItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemResource),
ComputerToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ComputerToolCallItemResource),
ComputerToolCallOutputItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemResource),
WebSearchToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.WebSearchToolCallItemResource),
ReasoningItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ReasoningItemResource),
ItemReferenceItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ItemReferenceItemResource),
ImageGenerationToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemResource),
CodeInterpreterToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemResource),
LocalShellToolCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.LocalShellToolCallItemResource),
LocalShellToolCallOutputItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemResource),
MCPListToolsItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPListToolsItemResource),
MCPApprovalRequestItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource),
MCPApprovalResponseItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource),
MCPCallItemResource.ItemType => doc.Deserialize(OpenAIHostingJsonContext.Default.MCPCallItemResource),
_ => null
};
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ItemResource value, JsonSerializerOptions options)
{
// Directly serialize using the appropriate type info from the context
switch (value)
{
case ResponsesMessageItemResource message:
JsonSerializer.Serialize(writer, message, this._context.ResponsesMessageItemResource);
JsonSerializer.Serialize(writer, message, OpenAIHostingJsonContext.Default.ResponsesMessageItemResource);
break;
case FileSearchToolCallItemResource fileSearch:
JsonSerializer.Serialize(writer, fileSearch, this._context.FileSearchToolCallItemResource);
JsonSerializer.Serialize(writer, fileSearch, OpenAIHostingJsonContext.Default.FileSearchToolCallItemResource);
break;
case FunctionToolCallItemResource functionCall:
JsonSerializer.Serialize(writer, functionCall, this._context.FunctionToolCallItemResource);
JsonSerializer.Serialize(writer, functionCall, OpenAIHostingJsonContext.Default.FunctionToolCallItemResource);
break;
case FunctionToolCallOutputItemResource functionOutput:
JsonSerializer.Serialize(writer, functionOutput, this._context.FunctionToolCallOutputItemResource);
JsonSerializer.Serialize(writer, functionOutput, OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemResource);
break;
case ComputerToolCallItemResource computerCall:
JsonSerializer.Serialize(writer, computerCall, this._context.ComputerToolCallItemResource);
JsonSerializer.Serialize(writer, computerCall, OpenAIHostingJsonContext.Default.ComputerToolCallItemResource);
break;
case ComputerToolCallOutputItemResource computerOutput:
JsonSerializer.Serialize(writer, computerOutput, this._context.ComputerToolCallOutputItemResource);
JsonSerializer.Serialize(writer, computerOutput, OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemResource);
break;
case WebSearchToolCallItemResource webSearch:
JsonSerializer.Serialize(writer, webSearch, this._context.WebSearchToolCallItemResource);
JsonSerializer.Serialize(writer, webSearch, OpenAIHostingJsonContext.Default.WebSearchToolCallItemResource);
break;
case ReasoningItemResource reasoning:
JsonSerializer.Serialize(writer, reasoning, this._context.ReasoningItemResource);
JsonSerializer.Serialize(writer, reasoning, OpenAIHostingJsonContext.Default.ReasoningItemResource);
break;
case ItemReferenceItemResource itemReference:
JsonSerializer.Serialize(writer, itemReference, this._context.ItemReferenceItemResource);
JsonSerializer.Serialize(writer, itemReference, OpenAIHostingJsonContext.Default.ItemReferenceItemResource);
break;
case ImageGenerationToolCallItemResource imageGeneration:
JsonSerializer.Serialize(writer, imageGeneration, this._context.ImageGenerationToolCallItemResource);
JsonSerializer.Serialize(writer, imageGeneration, OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemResource);
break;
case CodeInterpreterToolCallItemResource codeInterpreter:
JsonSerializer.Serialize(writer, codeInterpreter, this._context.CodeInterpreterToolCallItemResource);
JsonSerializer.Serialize(writer, codeInterpreter, OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemResource);
break;
case LocalShellToolCallItemResource localShell:
JsonSerializer.Serialize(writer, localShell, this._context.LocalShellToolCallItemResource);
JsonSerializer.Serialize(writer, localShell, OpenAIHostingJsonContext.Default.LocalShellToolCallItemResource);
break;
case LocalShellToolCallOutputItemResource localShellOutput:
JsonSerializer.Serialize(writer, localShellOutput, this._context.LocalShellToolCallOutputItemResource);
JsonSerializer.Serialize(writer, localShellOutput, OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemResource);
break;
case MCPListToolsItemResource mcpListTools:
JsonSerializer.Serialize(writer, mcpListTools, this._context.MCPListToolsItemResource);
JsonSerializer.Serialize(writer, mcpListTools, OpenAIHostingJsonContext.Default.MCPListToolsItemResource);
break;
case MCPApprovalRequestItemResource mcpApprovalRequest:
JsonSerializer.Serialize(writer, mcpApprovalRequest, this._context.MCPApprovalRequestItemResource);
JsonSerializer.Serialize(writer, mcpApprovalRequest, OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource);
break;
case MCPApprovalResponseItemResource mcpApprovalResponse:
JsonSerializer.Serialize(writer, mcpApprovalResponse, this._context.MCPApprovalResponseItemResource);
JsonSerializer.Serialize(writer, mcpApprovalResponse, OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource);
break;
case MCPCallItemResource mcpCall:
JsonSerializer.Serialize(writer, mcpCall, this._context.MCPCallItemResource);
JsonSerializer.Serialize(writer, mcpCall, OpenAIHostingJsonContext.Default.MCPCallItemResource);
break;
default:
throw new JsonException($"Unknown item type: {value.GetType().Name}");
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
/// <summary>
/// JSON converter for ResponsesMessageItemParam that handles role-based polymorphic deserialization.
/// </summary>
internal sealed class ResponsesMessageItemParamConverter : JsonConverter<ResponsesMessageItemParam>
{
/// <inheritdoc/>
public override ResponsesMessageItemParam? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
if (!root.TryGetProperty("role", out var roleElement))
{
throw new JsonException("ResponsesMessageItemParam must have a 'role' property");
}
var role = roleElement.GetString();
return role switch
{
"user" => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesUserMessageItemParam),
"assistant" => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemParam),
"system" => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemParam),
"developer" => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemParam),
_ => throw new JsonException($"Unknown message role: {role}")
};
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ResponsesMessageItemParam value, JsonSerializerOptions options)
{
switch (value)
{
case ResponsesUserMessageItemParam user:
JsonSerializer.Serialize(writer, user, OpenAIHostingJsonContext.Default.ResponsesUserMessageItemParam);
break;
case ResponsesAssistantMessageItemParam assistant:
JsonSerializer.Serialize(writer, assistant, OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemParam);
break;
case ResponsesSystemMessageItemParam system:
JsonSerializer.Serialize(writer, system, OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemParam);
break;
case ResponsesDeveloperMessageItemParam developer:
JsonSerializer.Serialize(writer, developer, OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemParam);
break;
default:
throw new JsonException($"Unknown message type: {value.GetType().Name}");
}
}
}
@@ -14,82 +14,47 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
[ExcludeFromCodeCoverage]
internal sealed class ResponsesMessageItemResourceConverter : JsonConverter<ResponsesMessageItemResource>
{
private readonly ResponsesJsonContext _context;
/// <summary>
/// Initializes a new instance of the <see cref="ResponsesMessageItemResourceConverter"/> class.
/// </summary>
public ResponsesMessageItemResourceConverter()
{
this._context = ResponsesJsonContext.Default;
}
/// <inheritdoc/>
public override ResponsesMessageItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Clone the reader to peek at the JSON
Utf8JsonReader readerClone = reader;
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
// Read through the JSON to find the role property
string? role = null;
if (readerClone.TokenType != JsonTokenType.StartObject)
if (!root.TryGetProperty("role", out var roleElement))
{
throw new JsonException("Expected start of object");
throw new JsonException("ResponsesMessageItemResource must have a 'role' property");
}
while (readerClone.Read())
{
if (readerClone.TokenType == JsonTokenType.EndObject)
{
break;
}
if (readerClone.TokenType == JsonTokenType.PropertyName)
{
string propertyName = readerClone.GetString()!;
readerClone.Read(); // Move to the value
if (propertyName == "role")
{
role = readerClone.GetString();
break;
}
if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
{
// Skip nested objects/arrays
readerClone.Skip();
}
}
}
var role = roleElement.GetString();
// Determine the concrete type based on the role and deserialize using the source generation context
return role switch
{
ResponsesAssistantMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesAssistantMessageItemResource),
ResponsesUserMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesUserMessageItemResource),
ResponsesSystemMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesSystemMessageItemResource),
ResponsesDeveloperMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesDeveloperMessageItemResource),
ResponsesAssistantMessageItemResource.RoleType => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemResource),
ResponsesUserMessageItemResource.RoleType => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesUserMessageItemResource),
ResponsesSystemMessageItemResource.RoleType => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemResource),
ResponsesDeveloperMessageItemResource.RoleType => doc.Deserialize(OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemResource),
_ => throw new JsonException($"Unknown message role: {role}")
};
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ResponsesMessageItemResource value, JsonSerializerOptions options)
{
// Directly serialize using the appropriate type info from the context
switch (value)
{
case ResponsesAssistantMessageItemResource assistant:
JsonSerializer.Serialize(writer, assistant, this._context.ResponsesAssistantMessageItemResource);
JsonSerializer.Serialize(writer, assistant, OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemResource);
break;
case ResponsesUserMessageItemResource user:
JsonSerializer.Serialize(writer, user, this._context.ResponsesUserMessageItemResource);
JsonSerializer.Serialize(writer, user, OpenAIHostingJsonContext.Default.ResponsesUserMessageItemResource);
break;
case ResponsesSystemMessageItemResource system:
JsonSerializer.Serialize(writer, system, this._context.ResponsesSystemMessageItemResource);
JsonSerializer.Serialize(writer, system, OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemResource);
break;
case ResponsesDeveloperMessageItemResource developer:
JsonSerializer.Serialize(writer, developer, this._context.ResponsesDeveloperMessageItemResource);
JsonSerializer.Serialize(writer, developer, OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemResource);
break;
default:
throw new JsonException($"Unknown message type: {value.GetType().Name}");
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -11,9 +10,11 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
/// JSON converter for enums that uses snake_case naming convention.
/// </summary>
/// <typeparam name="T">The enum type to convert.</typeparam>
[ExcludeFromCodeCoverage]
internal sealed class SnakeCaseEnumConverter<T> : JsonStringEnumConverter<T> where T : struct, Enum
{
/// <summary>
/// Creates a new instance of the <see cref="SnakeCaseEnumConverter{T}"/> class.
/// </summary>
public SnakeCaseEnumConverter() : base(JsonNamingPolicy.SnakeCaseLower)
{
}
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Response executor that routes requests to hosted AIAgent services based on the model or agent.name parameter.
/// This executor resolves agents from keyed services registered via AddAIAgent().
/// </summary>
internal sealed class HostedAgentResponseExecutor : IResponseExecutor
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<HostedAgentResponseExecutor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="HostedAgentResponseExecutor"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider used to resolve hosted agents.</param>
/// <param name="logger">The logger instance.</param>
public HostedAgentResponseExecutor(
IServiceProvider serviceProvider,
ILogger<HostedAgentResponseExecutor> logger)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
ArgumentNullException.ThrowIfNull(logger);
this._serviceProvider = serviceProvider;
this._logger = logger;
}
/// <inheritdoc/>
public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
AgentInvocationContext context,
CreateResponse request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Validate and resolve agent synchronously to ensure validation errors are thrown immediately
AIAgent agent = this.ResolveAgent(request);
// Create options with properties from the request
var chatOptions = new ChatOptions
{
ConversationId = request.Conversation?.Id,
Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
Instructions = request.Instructions,
ModelId = request.Model,
};
var options = new ChatClientAgentRunOptions(chatOptions);
// Convert input to chat messages
var messages = new List<ChatMessage>();
foreach (var inputMessage in request.Input.GetInputMessages())
{
messages.Add(inputMessage.ToChatMessage());
}
// Use the extension method to convert streaming updates to streaming response events
await foreach (var streamingEvent in agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken)
.ToStreamingResponseAsync(request, context, cancellationToken).ConfigureAwait(false))
{
yield return streamingEvent;
}
}
/// <summary>
/// Resolves an agent from the service provider based on the request.
/// </summary>
/// <param name="request">The create response request.</param>
/// <returns>The resolved AIAgent instance.</returns>
/// <exception cref="InvalidOperationException">Thrown when the agent cannot be resolved.</exception>
private AIAgent ResolveAgent(CreateResponse request)
{
// Extract agent name from agent.name or model parameter
var agentName = request.Agent?.Name ?? request.Model;
if (string.IsNullOrEmpty(agentName))
{
throw new InvalidOperationException("No 'agent.name' or 'model' specified in the request.");
}
// Resolve the keyed agent service
try
{
return this._serviceProvider.GetRequiredKeyedService<AIAgent>(agentName);
}
catch (InvalidOperationException ex)
{
this._logger.LogError(ex, "Failed to resolve agent with name '{AgentName}'", agentName);
throw new InvalidOperationException($"Agent '{agentName}' not found. Ensure the agent is registered with AddAIAgent().", ex);
}
}
/// <summary>
/// Validates that the agent can be resolved without actually resolving it.
/// This allows early validation before starting async execution.
/// </summary>
/// <param name="request">The create response request.</param>
/// <exception cref="InvalidOperationException">Thrown when the agent cannot be resolved.</exception>
public void ValidateAgent(CreateResponse request)
{
// Use the same logic as ResolveAgent but don't return the agent
_ = this.ResolveAgent(request);
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Interface for executing response generation.
/// Implementations can use local execution (AIAgent) or forward to remote workers.
/// </summary>
internal interface IResponseExecutor
{
/// <summary>
/// Executes a response generation request and returns streaming events.
/// </summary>
/// <param name="context">The agent invocation context containing the ID generator and other context information.</param>
/// <param name="request">The create response request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of streaming response events.</returns>
IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
AgentInvocationContext context,
CreateResponse request,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Service interface for handling OpenAI Responses API operations.
/// Implementations can use various storage and execution strategies (in-memory, Orleans grains, etc.).
/// </summary>
internal interface IResponsesService
{
/// <summary>
/// Default limit for list operations.
/// </summary>
const int DefaultListLimit = 20;
/// <summary>
/// Creates a model response for the given input.
/// </summary>
/// <param name="request">The create response request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created response.</returns>
Task<Response> CreateResponseAsync(
CreateResponse request,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a streaming model response for the given input.
/// </summary>
/// <param name="request">The create response request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of streaming response events.</returns>
IAsyncEnumerable<StreamingResponseEvent> CreateResponseStreamingAsync(
CreateResponse request,
CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a response by ID.
/// </summary>
/// <param name="responseId">The ID of the response to retrieve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The response if found, null otherwise.</returns>
Task<Response?> GetResponseAsync(
string responseId,
CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a response by ID in streaming mode, yielding events as they become available.
/// </summary>
/// <param name="responseId">The ID of the response to retrieve.</param>
/// <param name="startingAfter">The sequence number after which to start streaming. If null, starts from the beginning.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of streaming updates.</returns>
IAsyncEnumerable<StreamingResponseEvent> GetResponseStreamingAsync(
string responseId,
int? startingAfter = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Cancels an in-progress response.
/// </summary>
/// <param name="responseId">The ID of the response to cancel.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The updated response after cancellation.</returns>
Task<Response> CancelResponseAsync(
string responseId,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a response by ID.
/// </summary>
/// <param name="responseId">The ID of the response to delete.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the response was deleted, false if it was not found.</returns>
Task<bool> DeleteResponseAsync(
string responseId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists the input items for a response.
/// </summary>
/// <param name="responseId">The ID of the response.</param>
/// <param name="limit">Maximum number of items to return (1-100). Defaults to <see cref="DefaultListLimit"/> if null.</param>
/// <param name="order">Sort order. Defaults to <see cref="SortOrder.Descending"/> if null.</param>
/// <param name="after">Return items after this ID.</param>
/// <param name="before">Return items before this ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list response with items and pagination info.</returns>
Task<ListResponse<ItemResource>> ListResponseInputItemsAsync(
string responseId,
int? limit = null,
SortOrder? order = null,
string? after = null,
string? before = null,
CancellationToken cancellationToken = default);
}
@@ -1,82 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Generates IDs with partition keys.
/// </summary>
internal sealed partial class IdGenerator
{
private readonly string _partitionId;
/// <summary>
/// Initializes a new instance of the <see cref="IdGenerator"/> class.
/// </summary>
/// <param name="responseId">The response ID.</param>
/// <param name="conversationId">The conversation ID.</param>
public IdGenerator(string? responseId, string? conversationId)
{
this.ResponseId = responseId ?? IdGeneratorHelpers.NewId("resp");
this.ConversationId = conversationId ?? IdGeneratorHelpers.NewId("conv");
this._partitionId = IdGeneratorHelpers.GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
}
/// <summary>
/// Creates a new ID generator from a create response request.
/// </summary>
/// <param name="request">The create response request.</param>
/// <returns>A new ID generator.</returns>
public static IdGenerator From(CreateResponse request)
{
string? responseId = null;
request.Metadata?.TryGetValue("response_id", out responseId);
return new IdGenerator(responseId, request.Conversation?.Id);
}
/// <summary>
/// Gets the response ID.
/// </summary>
public string ResponseId { get; }
/// <summary>
/// Gets the conversation ID.
/// </summary>
public string ConversationId { get; }
/// <summary>
/// Generates a new ID.
/// </summary>
/// <param name="category">The optional category for the ID.</param>
/// <returns>A generated ID string.</returns>
public string Generate(string? category = null)
{
var prefix = string.IsNullOrEmpty(category) ? "id" : category;
return IdGeneratorHelpers.NewId(prefix, partitionKey: this._partitionId);
}
/// <summary>
/// Generates a function call ID.
/// </summary>
/// <returns>A function call ID.</returns>
public string GenerateFunctionCallId() => this.Generate("func");
/// <summary>
/// Generates a function output ID.
/// </summary>
/// <returns>A function output ID.</returns>
public string GenerateFunctionOutputId() => this.Generate("funcout");
/// <summary>
/// Generates a message ID.
/// </summary>
/// <returns>A message ID.</returns>
public string GenerateMessageId() => this.Generate("msg");
/// <summary>
/// Generates a reasoning ID.
/// </summary>
/// <returns>A reasoning ID.</returns>
public string GenerateReasoningId() => this.Generate("rs");
}
@@ -0,0 +1,545 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.Caching.Memory;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// In-memory implementation of responses service for testing and development.
/// This implementation is thread-safe but data is not persisted across application restarts.
/// </summary>
internal sealed class InMemoryResponsesService : IResponsesService, IDisposable
{
private readonly IResponseExecutor _executor;
private readonly MemoryCache _cache;
private readonly InMemoryStorageOptions _options;
private readonly Conversations.IConversationStorage? _conversationStorage;
private sealed class ResponseState
{
private readonly object _lock = new();
private TaskCompletionSource _updateSignal = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly Dictionary<int, ItemResource> _outputItems = [];
public Response? Response { get; set; }
public CreateResponse? Request { get; set; }
public List<StreamingResponseEvent> StreamingUpdates { get; } = [];
public Task? CompletionTask { get; set; }
public CancellationTokenSource? CancellationTokenSource { get; set; }
public bool IsTerminal => this.Response?.IsTerminal ?? false;
public void AddStreamingEvent(StreamingResponseEvent streamingEvent)
{
lock (this._lock)
{
this.StreamingUpdates.Add(streamingEvent);
// Update the response object for events that contain it
if (streamingEvent is IStreamingResponseEventWithResponse responseEvent)
{
this.Response = responseEvent.Response;
}
// Track output items as they're added or updated
if (streamingEvent is StreamingOutputItemAdded itemAdded)
{
this._outputItems[itemAdded.OutputIndex] = itemAdded.Item;
this.UpdateResponseOutput();
}
else if (streamingEvent is StreamingOutputItemDone itemDone)
{
this._outputItems[itemDone.OutputIndex] = itemDone.Item;
this.UpdateResponseOutput();
}
}
this.SignalUpdate();
}
private void UpdateResponseOutput()
{
// Update the Response.Output list with current items
if (this.Response is not null)
{
List<ItemResource> outputList = [.. this._outputItems.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value)];
this.Response = this.Response with { Output = outputList };
}
}
public async IAsyncEnumerable<StreamingResponseEvent> StreamUpdatesAsync(
int startingAfter = 0,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int streamedCount = startingAfter;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// Capture the wait task before checking state to avoid race conditions
Task waitTask = this.WaitForUpdateAsync(cancellationToken);
// Copy any new updates and check terminal state while holding the lock
List<StreamingResponseEvent> newUpdates;
bool isTerminal;
lock (this._lock)
{
newUpdates = this.StreamingUpdates.Skip(streamedCount).ToList();
streamedCount += newUpdates.Count;
isTerminal = this.IsTerminal;
}
// Yield the updates outside the lock
foreach (StreamingResponseEvent update in newUpdates)
{
yield return update;
}
// Check if we're done (after yielding any final events)
if (isTerminal)
{
break;
}
// Wait for the next update to be signaled
await waitTask.ConfigureAwait(false);
}
}
private Task WaitForUpdateAsync(CancellationToken cancellationToken)
{
Task signalTask = this._updateSignal.Task;
return signalTask.WaitAsync(cancellationToken);
}
internal void SignalUpdate()
{
TaskCompletionSource oldSignal = Interlocked.Exchange(ref this._updateSignal, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously));
oldSignal.TrySetResult();
}
}
public InMemoryResponsesService(IResponseExecutor executor)
: this(executor, new InMemoryStorageOptions(), null)
{
}
public InMemoryResponsesService(IResponseExecutor executor, InMemoryStorageOptions options)
: this(executor, options, null)
{
}
public InMemoryResponsesService(IResponseExecutor executor, InMemoryStorageOptions options, Conversations.IConversationStorage? conversationStorage)
{
ArgumentNullException.ThrowIfNull(executor);
ArgumentNullException.ThrowIfNull(options);
this._executor = executor;
this._options = options;
this._cache = new MemoryCache(options.ToMemoryCacheOptions());
this._conversationStorage = conversationStorage;
}
public async Task<Response> CreateResponseAsync(
CreateResponse request,
CancellationToken cancellationToken = default)
{
ValidateRequest(request);
// Validate agent resolution early for HostedAgentResponseExecutor
if (this._executor is HostedAgentResponseExecutor hostedExecutor)
{
hostedExecutor.ValidateAgent(request);
}
if (request.Stream == true)
{
throw new InvalidOperationException("Cannot create a streaming response using CreateResponseAsync. Use CreateResponseStreamingAsync instead.");
}
var idGenerator = new IdGenerator(responseId: null, conversationId: request.Conversation?.Id);
var responseId = idGenerator.ResponseId;
var state = this.InitializeResponse(responseId, request);
var ct = request.Background switch
{
true => CancellationToken.None,
_ => cancellationToken,
};
state.CompletionTask = this.ExecuteResponseAsync(responseId, state, ct);
// For background responses, start execution and return immediately
if (request.Background == true)
{
return state.Response!;
}
// For non-background responses, wait for completion
await state.CompletionTask!.WaitAsync(cancellationToken).ConfigureAwait(false);
return state.Response!;
}
public async IAsyncEnumerable<StreamingResponseEvent> CreateResponseStreamingAsync(
CreateResponse request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ValidateRequest(request);
if (request.Stream == false)
{
throw new InvalidOperationException("Cannot create a non-streaming response using CreateResponseStreamingAsync. Use CreateResponseAsync instead.");
}
var idGenerator = new IdGenerator(responseId: null, conversationId: request.Conversation?.Id);
var responseId = idGenerator.ResponseId;
var state = this.InitializeResponse(responseId, request);
// Start execution
state.CompletionTask = this.ExecuteResponseAsync(responseId, state, CancellationToken.None);
// Stream updates as they become available
await foreach (StreamingResponseEvent update in state.StreamUpdatesAsync(cancellationToken: cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
public Task<Response?> GetResponseAsync(string responseId, CancellationToken cancellationToken = default)
{
this._cache.TryGetValue(responseId, out ResponseState? state);
return Task.FromResult(state?.Response);
}
public async IAsyncEnumerable<StreamingResponseEvent> GetResponseStreamingAsync(
string responseId,
int? startingAfter = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (!this._cache.TryGetValue(responseId, out ResponseState? state) || state is null)
{
yield break;
}
// Stream existing updates starting from the specified position
await foreach (StreamingResponseEvent update in state.StreamUpdatesAsync(startingAfter ?? 0, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
public async Task<Response> CancelResponseAsync(string responseId, CancellationToken cancellationToken = default)
{
if (!this._cache.TryGetValue(responseId, out ResponseState? state) || state is null)
{
throw new InvalidOperationException($"Response '{responseId}' not found.");
}
if (state.Response is null || state.Response.Background != true)
{
throw new InvalidOperationException($"Only background responses can be cancelled. Response '{responseId}' was not created with background=true.");
}
if (state.IsTerminal)
{
throw new InvalidOperationException($"Response '{responseId}' is already in a terminal state and cannot be cancelled.");
}
// Cancel the execution
state.CancellationTokenSource?.Cancel();
if (state.CompletionTask is { } task)
{
await task.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
return state.Response;
}
public Task<bool> DeleteResponseAsync(string responseId, CancellationToken cancellationToken = default)
{
if (!this._cache.TryGetValue(responseId, out ResponseState? state))
{
return Task.FromResult(false);
}
// Cancel any ongoing execution
state?.CancellationTokenSource?.Cancel();
// Remove the response
this._cache.Remove(responseId);
return Task.FromResult(true);
}
public Task<ListResponse<ItemResource>> ListResponseInputItemsAsync(
string responseId,
int? limit = null,
SortOrder? order = null,
string? after = null,
string? before = null,
CancellationToken cancellationToken = default)
{
int effectiveLimit = Math.Clamp(limit ?? IResponsesService.DefaultListLimit, 1, 100);
SortOrder effectiveOrder = order ?? SortOrder.Descending;
if (!this._cache.TryGetValue(responseId, out ResponseState? state))
{
throw new InvalidOperationException($"Response '{responseId}' not found.");
}
if (state is null)
{
throw new InvalidOperationException($"Response '{responseId}' state is null.");
}
var itemResources = GetInputItems(responseId, state);
// Apply ordering
if (effectiveOrder == SortOrder.Descending)
{
itemResources.Reverse();
}
// Apply pagination
var filtered = itemResources.AsEnumerable();
if (!string.IsNullOrEmpty(after))
{
int afterIndex = itemResources.FindIndex(m => m.Id == after);
if (afterIndex >= 0)
{
filtered = itemResources.Skip(afterIndex + 1);
}
}
if (!string.IsNullOrEmpty(before))
{
int beforeIndex = itemResources.FindIndex(m => m.Id == before);
if (beforeIndex >= 0)
{
filtered = filtered.Take(beforeIndex);
}
}
var result = filtered.Take(effectiveLimit + 1).ToList();
var hasMore = result.Count > effectiveLimit;
if (hasMore)
{
result = result.Take(effectiveLimit).ToList();
}
return Task.FromResult(new ListResponse<ItemResource>
{
Data = result,
FirstId = result.FirstOrDefault()?.Id,
LastId = result.LastOrDefault()?.Id,
HasMore = hasMore
});
}
private static void ValidateRequest(CreateResponse request)
{
if (request.Conversation is not null && !string.IsNullOrEmpty(request.Conversation.Id) &&
!string.IsNullOrEmpty(request.PreviousResponseId))
{
throw new InvalidOperationException("Mutually exclusive parameters: 'conversation' and 'previous_response_id'. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.");
}
}
private ResponseState InitializeResponse(string responseId, CreateResponse request)
{
var metadata = request.Metadata ?? [];
// Create initial response
// Background responses always start as "queued", non-background as "in_progress"
var initialStatus = request.Background is true ? ResponseStatus.Queued : ResponseStatus.InProgress;
var response = new Response
{
Agent = request.Agent?.ToAgentId(),
Background = request.Background,
Conversation = request.Conversation,
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Error = null,
Id = responseId,
IncompleteDetails = null,
Instructions = request.Instructions,
MaxOutputTokens = request.MaxOutputTokens,
MaxToolCalls = request.MaxToolCalls,
Metadata = metadata,
Model = request.Model ?? "default",
Output = [],
ParallelToolCalls = request.ParallelToolCalls ?? true,
PreviousResponseId = request.PreviousResponseId,
Prompt = request.Prompt,
PromptCacheKey = request.PromptCacheKey,
Reasoning = request.Reasoning,
SafetyIdentifier = request.SafetyIdentifier,
ServiceTier = request.ServiceTier,
Status = initialStatus,
Store = request.Store,
Temperature = request.Temperature,
Text = request.Text,
ToolChoice = request.ToolChoice,
Tools = [.. request.Tools ?? []],
TopLogprobs = request.TopLogprobs,
TopP = request.TopP,
Truncation = request.Truncation,
Usage = ResponseUsage.Zero,
#pragma warning disable CS0618 // Type or member is obsolete
User = request.User
#pragma warning restore CS0618 // Type or member is obsolete
};
var state = new ResponseState
{
Response = response,
Request = request,
CancellationTokenSource = new CancellationTokenSource()
};
var entryOptions = this._options.ToMemoryCacheEntryOptions();
entryOptions.RegisterPostEvictionCallback((key, value, reason, state) =>
{
if (value is ResponseState responseState)
{
responseState.CancellationTokenSource?.Cancel();
}
});
this._cache.Set(responseId, state, entryOptions);
return state;
}
private async Task ExecuteResponseAsync(string responseId, ResponseState state, CancellationToken cancellationToken)
{
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var request = state.Request!;
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, state.CancellationTokenSource!.Token);
try
{
// Create agent invocation context
var context = new AgentInvocationContext(new IdGenerator(responseId: responseId, conversationId: state.Response?.Conversation?.Id));
// Collect output items for conversation storage
List<ItemResource> outputItems = [];
// Execute using the injected executor
await foreach (var streamingEvent in this._executor.ExecuteAsync(context, request, linkedCts.Token).ConfigureAwait(false))
{
state.AddStreamingEvent(streamingEvent);
// Collect output items
if (streamingEvent is StreamingOutputItemDone itemDone)
{
outputItems.Add(itemDone.Item);
}
}
// Add both input and output items to conversation storage if available
// This happens AFTER successful execution, in line with OpenAI's behavior
if (this._conversationStorage is not null && request.Conversation?.Id is not null)
{
var inputItems = GetInputItems(responseId, state);
var allItems = new List<ItemResource>(inputItems.Count + outputItems.Count);
allItems.AddRange(inputItems);
allItems.AddRange(outputItems);
if (allItems.Count > 0)
{
await this._conversationStorage.AddItemsAsync(request.Conversation.Id, allItems, linkedCts.Token).ConfigureAwait(false);
}
}
// Update response status to completed if not already in a terminal state
if (!state.IsTerminal)
{
state.Response = state.Response! with
{
Status = ResponseStatus.Completed
};
var sequenceNumber = state.StreamingUpdates.Count + 1;
var completedEvent = new StreamingResponseCompleted
{
SequenceNumber = sequenceNumber,
Response = state.Response
};
state.AddStreamingEvent(completedEvent);
}
}
catch (OperationCanceledException)
{
// Update response status to cancelled
state.Response = state.Response! with
{
Status = ResponseStatus.Cancelled
};
var sequenceNumber = state.StreamingUpdates.Count + 1;
var cancelledEvent = new StreamingResponseCancelled
{
SequenceNumber = sequenceNumber,
Response = state.Response
};
state.AddStreamingEvent(cancelledEvent);
}
catch (Exception ex)
{
// Update response status to failed
state.Response = state.Response! with
{
Status = ResponseStatus.Failed,
Error = new ResponseError
{
Code = "execution_error",
Message = ex.Message
}
};
var sequenceNumber = state.StreamingUpdates.Count + 1;
var failedEvent = new StreamingResponseFailed
{
SequenceNumber = sequenceNumber,
Response = state.Response
};
state.AddStreamingEvent(failedEvent);
}
finally
{
// Signal one final time to unblock any waiting consumers
state.SignalUpdate();
}
}
private static List<ItemResource> GetInputItems(string responseId, ResponseState state)
{
var itemResources = new List<ItemResource>();
if (state.Request is not null)
{
// Use a deterministic random seed. We add 1 to avoid clashing with the output message ids.
var randomSeed = responseId.GetHashCode() + 1;
var idGenerator = new IdGenerator(responseId: responseId, conversationId: state.Response?.Conversation?.Id, randomSeed: randomSeed);
foreach (var inputMessage in state.Request.Input.GetInputMessages())
{
itemResources.AddRange(inputMessage.ToItemResource(idGenerator));
}
}
return itemResources;
}
public void Dispose()
{
this._cache.Dispose();
}
}
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Represents an agent identifier.
/// </summary>
internal sealed record AgentId
internal sealed class AgentId
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentId"/> class.
@@ -44,7 +44,7 @@ internal sealed record AgentId
/// <summary>
/// Represents an agent ID type.
/// </summary>
internal sealed record AgentIdType
internal sealed class AgentIdType
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentIdType"/> class.
@@ -65,7 +65,7 @@ internal sealed record AgentIdType
/// <summary>
/// Represents an agent reference.
/// </summary>
internal sealed record AgentReference
internal sealed class AgentReference
{
/// <summary>
/// The type of the reference (e.g., "agent" or "agent_reference").
@@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// Represents a reference to a conversation, which can be either a conversation ID (string) or a conversation object.
/// </summary>
[JsonConverter(typeof(ConversationReferenceJsonConverter))]
internal sealed record ConversationReference
internal sealed class ConversationReference
{
/// <summary>
/// The conversation ID.
@@ -42,6 +42,7 @@ internal sealed record ConversationReference
/// </summary>
internal sealed class ConversationReferenceJsonConverter : JsonConverter<ConversationReference>
{
/// <inheritdoc/>
public override ConversationReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
@@ -61,7 +62,7 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter<Convers
if (root.TryGetProperty("metadata", out var metadataProp) && metadataProp.ValueKind == JsonValueKind.Object)
{
metadata = JsonSerializer.Deserialize(metadataProp.GetRawText(), ResponsesJsonContext.Default.DictionaryStringString);
metadata = JsonSerializer.Deserialize(metadataProp.GetRawText(), OpenAIHostingJsonContext.Default.DictionaryStringString);
}
return id is null ? null : ConversationReference.FromObject(id, metadata);
@@ -74,6 +75,7 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter<Convers
throw new JsonException($"Unexpected token type for ConversationReference: {reader.TokenType}");
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ConversationReference value, JsonSerializerOptions options)
{
if (value is null)
@@ -95,7 +97,7 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter<Convers
if (value.Metadata is not null)
{
writer.WritePropertyName("metadata");
JsonSerializer.Serialize(writer, value.Metadata, ResponsesJsonContext.Default.DictionaryStringString);
JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString);
}
writer.WriteEndObject();
}
@@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Request to create a model response.
/// </summary>
internal sealed record CreateResponse
internal sealed class CreateResponse
{
/// <summary>
/// Text, image, or file inputs to the model, used to generate a response.
@@ -65,7 +65,10 @@ internal sealed record CreateResponse
/// <summary>
/// The unique ID of the previous response to the model. Use this to create multi-turn conversations.
/// Cannot be used in conjunction with conversation.
/// Cannot be used in conjunction with conversation (mutually exclusive).
/// The previous_response_id determines the conversation thread context - it follows the response chain,
/// not any explicit conversation. Context is maintained through the chain even if the previous response
/// was created with a conversation.id.
/// </summary>
[JsonPropertyName("previous_response_id")]
public string? PreviousResponseId { get; init; }
@@ -98,13 +101,16 @@ internal sealed record CreateResponse
/// Specify additional output data to include in the model response.
/// </summary>
[JsonPropertyName("include")]
public IReadOnlyList<string>? Include { get; init; }
public List<string>? Include { get; init; }
/// <summary>
/// The conversation that this response belongs to. Items from this conversation are prepended
/// to input_items for this response request.
/// Can be either a conversation ID (string) or a conversation object with ID and optional metadata.
/// Input items and output items from this response are automatically added to this conversation after this response completes.
/// Cannot be used in conjunction with previous_response_id (mutually exclusive).
/// Use conversation.id for explicit conversation boundaries and starting new threads.
/// Use previous_response_id for simple linear conversation chaining.
/// </summary>
[JsonPropertyName("conversation")]
public ConversationReference? Conversation { get; init; }
@@ -178,7 +184,7 @@ internal sealed record CreateResponse
/// An array of tools the model may call while generating a response.
/// </summary>
[JsonPropertyName("tools")]
public IReadOnlyList<JsonElement>? Tools { get; init; }
public List<JsonElement>? Tools { get; init; }
/// <summary>
/// How the model should select which tool (or tools) to use when generating a response.
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// A message input to the model with a role indicating instruction following hierarchy.
/// Aligns with the OpenAI Responses API InputMessage/EasyInputMessage schema.
/// </summary>
internal sealed record InputMessage
internal sealed class InputMessage
{
/// <summary>
/// The role of the message input. One of user, assistant, system, or developer.
@@ -22,7 +22,7 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
this.Contents = null;
}
private InputMessageContent(IReadOnlyList<ItemContent> contents)
private InputMessageContent(List<ItemContent> contents)
{
this.Contents = contents ?? throw new ArgumentNullException(nameof(contents));
this.Text = null;
@@ -36,12 +36,12 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
/// <summary>
/// Creates an InputMessageContent from a list of ItemContent items.
/// </summary>
public static InputMessageContent FromContents(IReadOnlyList<ItemContent> contents) => new(contents);
public static InputMessageContent FromContents(List<ItemContent> contents) => new(contents);
/// <summary>
/// Creates an InputMessageContent from a list of ItemContent items.
/// </summary>
public static InputMessageContent FromContents(params ItemContent[] contents) => new(contents);
public static InputMessageContent FromContents(params ItemContent[] contents) => new([.. contents]);
/// <summary>
/// Implicit conversion from string to InputMessageContent.
@@ -62,12 +62,14 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
/// Gets whether this content is text.
/// </summary>
[MemberNotNullWhen(true, nameof(Text))]
[MemberNotNullWhen(false, nameof(Contents))]
public bool IsText => this.Text is not null;
/// <summary>
/// Gets whether this content is a list of ItemContent items.
/// </summary>
[MemberNotNullWhen(true, nameof(Contents))]
[MemberNotNullWhen(false, nameof(Text))]
public bool IsContents => this.Contents is not null;
/// <summary>
@@ -78,7 +80,7 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
/// <summary>
/// Gets the ItemContent items, or null if this is not a content list.
/// </summary>
public IReadOnlyList<ItemContent>? Contents { get; }
public List<ItemContent>? Contents { get; }
/// <inheritdoc/>
public bool Equals(InputMessageContent? other)
@@ -143,6 +145,16 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
{
return !Equals(left, right);
}
/// <summary>
/// Converts this instance to a list of ItemContent.
/// </summary>
public List<ItemContent> ToItemContents()
{
return this.IsText
? [new ItemContentInputText { Text = this.Text }]
: this.Contents;
}
}
/// <summary>
@@ -150,6 +162,7 @@ internal sealed class InputMessageContent : IEquatable<InputMessageContent>
/// </summary>
internal sealed class InputMessageContentJsonConverter : JsonConverter<InputMessageContent>
{
/// <inheritdoc/>
public override InputMessageContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Check if it's a string
@@ -162,7 +175,7 @@ internal sealed class InputMessageContentJsonConverter : JsonConverter<InputMess
// Check if it's an array of ItemContent
if (reader.TokenType == JsonTokenType.StartArray)
{
var contents = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListItemContent);
var contents = JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ListItemContent);
return contents?.Count > 0
? InputMessageContent.FromContents(contents)
: InputMessageContent.FromText(string.Empty);
@@ -171,6 +184,7 @@ internal sealed class InputMessageContentJsonConverter : JsonConverter<InputMess
throw new JsonException($"Unexpected token type for InputMessageContent: {reader.TokenType}");
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, InputMessageContent value, JsonSerializerOptions options)
{
if (value.IsText)
@@ -179,7 +193,7 @@ internal sealed class InputMessageContentJsonConverter : JsonConverter<InputMess
}
else if (value.IsContents)
{
JsonSerializer.Serialize(writer, value.Contents, ResponsesJsonContext.Default.ListItemContent);
JsonSerializer.Serialize(writer, value.Contents, OpenAIHostingJsonContext.Default.ListItemContent);
}
else
{
@@ -0,0 +1,577 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Base class for all item parameters (input items for creating conversation items or response inputs).
/// Unlike ItemResource, ItemParam does not have an ID field - the server generates IDs upon creation.
/// </summary>
[JsonConverter(typeof(ItemParamConverter))]
internal abstract class ItemParam
{
/// <summary>
/// The type of the item.
/// </summary>
[JsonPropertyName("type")]
public abstract string Type { get; }
}
/// <summary>
/// Base class for message item parameters.
/// </summary>
[JsonConverter(typeof(ResponsesMessageItemParamConverter))]
internal abstract class ResponsesMessageItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for message items.
/// </summary>
public const string ItemType = "message";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The role of the message sender.
/// </summary>
[JsonPropertyName("role")]
public abstract ChatRole Role { get; }
}
/// <summary>
/// A user message item parameter.
/// </summary>
internal sealed class ResponsesUserMessageItemParam : ResponsesMessageItemParam
{
/// <summary>
/// The constant role type identifier for user messages.
/// </summary>
public const string RoleType = "user";
/// <inheritdoc/>
public override ChatRole Role => ChatRole.User;
/// <summary>
/// The content of the message. Can be a simple string or an array of content parts.
/// </summary>
[JsonPropertyName("content")]
public required InputMessageContent Content { get; init; }
}
/// <summary>
/// An assistant message item parameter.
/// </summary>
internal sealed class ResponsesAssistantMessageItemParam : ResponsesMessageItemParam
{
/// <summary>
/// The constant role type identifier for assistant messages.
/// </summary>
public const string RoleType = "assistant";
/// <inheritdoc/>
public override ChatRole Role => ChatRole.Assistant;
/// <summary>
/// The content of the message. Can be a simple string or an array of content parts.
/// </summary>
[JsonPropertyName("content")]
public required InputMessageContent Content { get; init; }
}
/// <summary>
/// A system message item parameter.
/// </summary>
internal sealed class ResponsesSystemMessageItemParam : ResponsesMessageItemParam
{
/// <summary>
/// The constant role type identifier for system messages.
/// </summary>
public const string RoleType = "system";
/// <inheritdoc/>
public override ChatRole Role => ChatRole.System;
/// <summary>
/// The content of the message. Can be a simple string or an array of content parts.
/// </summary>
[JsonPropertyName("content")]
public required InputMessageContent Content { get; init; }
}
/// <summary>
/// A developer message item parameter.
/// </summary>
internal sealed class ResponsesDeveloperMessageItemParam : ResponsesMessageItemParam
{
/// <summary>
/// The constant role type identifier for developer messages.
/// </summary>
public const string RoleType = "developer";
/// <inheritdoc/>
public override ChatRole Role => new(RoleType);
/// <summary>
/// The content of the message. Can be a simple string or an array of content parts.
/// </summary>
[JsonPropertyName("content")]
public required InputMessageContent Content { get; init; }
}
/// <summary>
/// A function tool call item parameter.
/// </summary>
internal sealed class FunctionToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for function call items.
/// </summary>
public const string ItemType = "function_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The call ID of the function.
/// </summary>
[JsonPropertyName("call_id")]
public required string CallId { get; init; }
/// <summary>
/// The name of the function.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// The arguments to the function.
/// </summary>
[JsonPropertyName("arguments")]
public required string Arguments { get; init; }
}
/// <summary>
/// A function tool call output item parameter.
/// </summary>
internal sealed class FunctionToolCallOutputItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for function call output items.
/// </summary>
public const string ItemType = "function_call_output";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The call ID of the function.
/// </summary>
[JsonPropertyName("call_id")]
public required string CallId { get; init; }
/// <summary>
/// The output of the function.
/// </summary>
[JsonPropertyName("output")]
public required string Output { get; init; }
}
/// <summary>
/// A file search tool call item parameter.
/// </summary>
internal sealed class FileSearchToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for file search call items.
/// </summary>
public const string ItemType = "file_search_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The queries used to search for files.
/// </summary>
[JsonPropertyName("queries")]
public List<string>? Queries { get; init; }
/// <summary>
/// The results of the file search tool call.
/// </summary>
[JsonPropertyName("results")]
public List<JsonElement>? Results { get; init; }
}
/// <summary>
/// A computer tool call item parameter.
/// </summary>
internal sealed class ComputerToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for computer call items.
/// </summary>
public const string ItemType = "computer_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// An identifier used when responding to the tool call with output.
/// </summary>
[JsonPropertyName("call_id")]
public required string CallId { get; init; }
/// <summary>
/// The action to perform.
/// </summary>
[JsonPropertyName("action")]
public required JsonElement Action { get; init; }
/// <summary>
/// The pending safety checks for the computer call.
/// </summary>
[JsonPropertyName("pending_safety_checks")]
public List<JsonElement>? PendingSafetyChecks { get; init; }
}
/// <summary>
/// A computer tool call output item parameter.
/// </summary>
internal sealed class ComputerToolCallOutputItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for computer call output items.
/// </summary>
public const string ItemType = "computer_call_output";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The ID of the computer tool call that produced the output.
/// </summary>
[JsonPropertyName("call_id")]
public required string CallId { get; init; }
/// <summary>
/// The safety checks reported by the API that have been acknowledged by the developer.
/// </summary>
[JsonPropertyName("acknowledged_safety_checks")]
public List<JsonElement>? AcknowledgedSafetyChecks { get; init; }
/// <summary>
/// The output of the computer tool call.
/// </summary>
[JsonPropertyName("output")]
public required JsonElement Output { get; init; }
}
/// <summary>
/// A web search tool call item parameter.
/// </summary>
internal sealed class WebSearchToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for web search call items.
/// </summary>
public const string ItemType = "web_search_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// An object describing the specific action taken in this web search call.
/// </summary>
[JsonPropertyName("action")]
public required JsonElement Action { get; init; }
}
/// <summary>
/// A reasoning item parameter.
/// </summary>
internal sealed class ReasoningItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for reasoning items.
/// </summary>
public const string ItemType = "reasoning";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The encrypted content of the reasoning item.
/// </summary>
[JsonPropertyName("encrypted_content")]
public string? EncryptedContent { get; init; }
/// <summary>
/// Reasoning text contents.
/// </summary>
[JsonPropertyName("summary")]
public List<JsonElement>? Summary { get; init; }
}
/// <summary>
/// An item reference item parameter.
/// </summary>
internal sealed class ItemReferenceItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for item reference items.
/// </summary>
public const string ItemType = "item_reference";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The service-originated ID of the previously generated response item being referenced.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
}
/// <summary>
/// An image generation tool call item parameter.
/// </summary>
internal sealed class ImageGenerationToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for image generation call items.
/// </summary>
public const string ItemType = "image_generation_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The generated image encoded in base64.
/// </summary>
[JsonPropertyName("result")]
public string? Result { get; init; }
}
/// <summary>
/// A code interpreter tool call item parameter.
/// </summary>
internal sealed class CodeInterpreterToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for code interpreter call items.
/// </summary>
public const string ItemType = "code_interpreter_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The ID of the container used to run the code.
/// </summary>
[JsonPropertyName("container_id")]
public string? ContainerId { get; init; }
/// <summary>
/// The code to run, or null if not available.
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; init; }
/// <summary>
/// The outputs generated by the code interpreter, such as logs or images.
/// Can be null if no outputs are available.
/// </summary>
[JsonPropertyName("outputs")]
public List<JsonElement>? Outputs { get; init; }
}
/// <summary>
/// A local shell tool call item parameter.
/// </summary>
internal sealed class LocalShellToolCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for local shell call items.
/// </summary>
public const string ItemType = "local_shell_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The unique ID of the local shell tool call generated by the model.
/// </summary>
[JsonPropertyName("call_id")]
public string? CallId { get; init; }
/// <summary>
/// The action to execute.
/// </summary>
[JsonPropertyName("action")]
public JsonElement? Action { get; init; }
}
/// <summary>
/// A local shell tool call output item parameter.
/// </summary>
internal sealed class LocalShellToolCallOutputItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for local shell call output items.
/// </summary>
public const string ItemType = "local_shell_call_output";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// A JSON string of the output of the local shell tool call.
/// </summary>
[JsonPropertyName("output")]
public string? Output { get; init; }
}
/// <summary>
/// An MCP list tools item parameter.
/// </summary>
internal sealed class MCPListToolsItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for MCP list tools items.
/// </summary>
public const string ItemType = "mcp_list_tools";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The tools available on the server.
/// </summary>
[JsonPropertyName("tools")]
public List<JsonElement>? Tools { get; init; }
/// <summary>
/// Error message if the server could not list tools.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
}
/// <summary>
/// An MCP approval request item parameter.
/// </summary>
internal sealed class MCPApprovalRequestItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for MCP approval request items.
/// </summary>
public const string ItemType = "mcp_approval_request";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server making the request.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The name of the tool to run.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// A JSON string of arguments for the tool.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; init; }
}
/// <summary>
/// An MCP approval response item parameter.
/// </summary>
internal sealed class MCPApprovalResponseItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for MCP approval response items.
/// </summary>
public const string ItemType = "mcp_approval_response";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The ID of the approval request being answered.
/// </summary>
[JsonPropertyName("approval_request_id")]
public string? ApprovalRequestId { get; init; }
/// <summary>
/// Whether the request was approved.
/// </summary>
[JsonPropertyName("approve")]
public bool? Approve { get; init; }
/// <summary>
/// Optional reason for the decision.
/// </summary>
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// An MCP call item parameter.
/// </summary>
internal sealed class MCPCallItemParam : ItemParam
{
/// <summary>
/// The constant item type identifier for MCP call items.
/// </summary>
public const string ItemType = "mcp_call";
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server running the tool.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The name of the tool that was run.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// A JSON string of the arguments passed to the tool.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; init; }
/// <summary>
/// The output from the tool call.
/// </summary>
[JsonPropertyName("output")]
public string? Output { get; init; }
/// <summary>
/// The error from the tool call, if any.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
}
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Extension methods for converting ItemParam (input) to ItemResource (output).
/// </summary>
internal static class ItemParamExtensions
{
/// <summary>
/// Converts an ItemParam (input model) to an ItemResource (output model) by adding server-generated fields.
/// </summary>
/// <param name="param">The input item parameter.</param>
/// <param name="idGenerator">The ID generator to use for creating item IDs.</param>
/// <returns>An ItemResource with a generated ID.</returns>
public static ItemResource ToItemResource(this ItemParam param, IdGenerator idGenerator)
{
ArgumentNullException.ThrowIfNull(param);
ArgumentNullException.ThrowIfNull(idGenerator);
string generatedId = idGenerator.GenerateMessageId();
return param switch
{
ResponsesUserMessageItemParam userMessageParam => new ResponsesUserMessageItemResource
{
Id = generatedId,
Content = userMessageParam.Content.ToItemContents(),
Status = ResponsesMessageItemResourceStatus.Completed
},
ResponsesSystemMessageItemParam systemMessageParam => new ResponsesSystemMessageItemResource
{
Id = generatedId,
Content = systemMessageParam.Content.ToItemContents(),
Status = ResponsesMessageItemResourceStatus.Completed
},
ResponsesAssistantMessageItemParam assistantMessageParam => new ResponsesAssistantMessageItemResource
{
Id = generatedId,
Content = assistantMessageParam.Content.ToItemContents(),
Status = ResponsesMessageItemResourceStatus.Completed
},
ResponsesDeveloperMessageItemParam developerMessageParam => new ResponsesDeveloperMessageItemResource
{
Id = generatedId,
Content = developerMessageParam.Content.ToItemContents(),
Status = ResponsesMessageItemResourceStatus.Completed
},
FunctionToolCallItemParam functionCallParam => new FunctionToolCallItemResource
{
Id = generatedId,
Name = functionCallParam.Name,
CallId = functionCallParam.CallId,
Arguments = functionCallParam.Arguments,
Status = FunctionToolCallItemResourceStatus.Completed
},
FunctionToolCallOutputItemParam functionOutputParam => new FunctionToolCallOutputItemResource
{
Id = generatedId,
CallId = functionOutputParam.CallId,
Output = functionOutputParam.Output
},
FileSearchToolCallItemParam fileSearchParam => new FileSearchToolCallItemResource
{
Id = generatedId,
Queries = fileSearchParam.Queries,
Results = fileSearchParam.Results
},
ComputerToolCallItemParam computerCallParam => new ComputerToolCallItemResource
{
Id = generatedId,
CallId = computerCallParam.CallId,
Action = computerCallParam.Action,
PendingSafetyChecks = computerCallParam.PendingSafetyChecks
},
ComputerToolCallOutputItemParam computerOutputParam => new ComputerToolCallOutputItemResource
{
Id = generatedId,
CallId = computerOutputParam.CallId,
AcknowledgedSafetyChecks = computerOutputParam.AcknowledgedSafetyChecks,
Output = computerOutputParam.Output
},
WebSearchToolCallItemParam webSearchParam => new WebSearchToolCallItemResource
{
Id = generatedId,
Action = webSearchParam.Action
},
ReasoningItemParam reasoningParam => new ReasoningItemResource
{
Id = generatedId,
EncryptedContent = reasoningParam.EncryptedContent,
Summary = reasoningParam.Summary
},
ItemReferenceItemParam => new ItemReferenceItemResource
{
Id = generatedId
},
ImageGenerationToolCallItemParam imageGenParam => new ImageGenerationToolCallItemResource
{
Id = generatedId,
Result = imageGenParam.Result
},
CodeInterpreterToolCallItemParam codeInterpreterParam => new CodeInterpreterToolCallItemResource
{
Id = generatedId,
ContainerId = codeInterpreterParam.ContainerId,
Code = codeInterpreterParam.Code,
Outputs = codeInterpreterParam.Outputs
},
LocalShellToolCallItemParam localShellParam => new LocalShellToolCallItemResource
{
Id = generatedId,
CallId = localShellParam.CallId,
Action = localShellParam.Action
},
LocalShellToolCallOutputItemParam localShellOutputParam => new LocalShellToolCallOutputItemResource
{
Id = generatedId,
Output = localShellOutputParam.Output
},
MCPListToolsItemParam mcpListToolsParam => new MCPListToolsItemResource
{
Id = generatedId,
ServerLabel = mcpListToolsParam.ServerLabel,
Tools = mcpListToolsParam.Tools,
Error = mcpListToolsParam.Error
},
MCPApprovalRequestItemParam mcpApprovalRequestParam => new MCPApprovalRequestItemResource
{
Id = generatedId,
ServerLabel = mcpApprovalRequestParam.ServerLabel,
Name = mcpApprovalRequestParam.Name,
Arguments = mcpApprovalRequestParam.Arguments
},
MCPApprovalResponseItemParam mcpApprovalResponseParam => new MCPApprovalResponseItemResource
{
Id = generatedId,
ApprovalRequestId = mcpApprovalResponseParam.ApprovalRequestId,
Approve = mcpApprovalResponseParam.Approve,
Reason = mcpApprovalResponseParam.Reason
},
MCPCallItemParam mcpCallParam => new MCPCallItemResource
{
Id = generatedId,
ServerLabel = mcpCallParam.ServerLabel,
Name = mcpCallParam.Name,
Arguments = mcpCallParam.Arguments,
Output = mcpCallParam.Output,
Error = mcpCallParam.Error
},
// Fallback for unknown types
_ => throw new InvalidOperationException($"Unknown ItemParam type: {param.GetType().Name}")
};
}
}
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// Base class for all item resources (output items from a response).
/// </summary>
[JsonConverter(typeof(ItemResourceConverter))]
internal abstract record ItemResource
internal abstract class ItemResource
{
/// <summary>
/// The unique identifier for the item.
@@ -31,7 +31,7 @@ internal abstract record ItemResource
/// Base class for message item resources.
/// </summary>
[JsonConverter(typeof(ResponsesMessageItemResourceConverter))]
internal abstract record ResponsesMessageItemResource : ItemResource
internal abstract class ResponsesMessageItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for message items.
@@ -57,7 +57,7 @@ internal abstract record ResponsesMessageItemResource : ItemResource
/// <summary>
/// An assistant message item resource.
/// </summary>
internal sealed record ResponsesAssistantMessageItemResource : ResponsesMessageItemResource
internal sealed class ResponsesAssistantMessageItemResource : ResponsesMessageItemResource
{
/// <summary>
/// The constant role type identifier for assistant messages.
@@ -71,13 +71,13 @@ internal sealed record ResponsesAssistantMessageItemResource : ResponsesMessageI
/// The content of the message.
/// </summary>
[JsonPropertyName("content")]
public required IList<ItemContent> Content { get; init; }
public required List<ItemContent> Content { get; init; }
}
/// <summary>
/// A user message item resource.
/// </summary>
internal sealed record ResponsesUserMessageItemResource : ResponsesMessageItemResource
internal sealed class ResponsesUserMessageItemResource : ResponsesMessageItemResource
{
/// <summary>
/// The constant role type identifier for user messages.
@@ -91,13 +91,13 @@ internal sealed record ResponsesUserMessageItemResource : ResponsesMessageItemRe
/// The content of the message.
/// </summary>
[JsonPropertyName("content")]
public required IList<ItemContent> Content { get; init; }
public required List<ItemContent> Content { get; init; }
}
/// <summary>
/// A system message item resource.
/// </summary>
internal sealed record ResponsesSystemMessageItemResource : ResponsesMessageItemResource
internal sealed class ResponsesSystemMessageItemResource : ResponsesMessageItemResource
{
/// <summary>
/// The constant role type identifier for system messages.
@@ -111,13 +111,13 @@ internal sealed record ResponsesSystemMessageItemResource : ResponsesMessageItem
/// The content of the message.
/// </summary>
[JsonPropertyName("content")]
public required IList<ItemContent> Content { get; init; }
public required List<ItemContent> Content { get; init; }
}
/// <summary>
/// A developer message item resource.
/// </summary>
internal sealed record ResponsesDeveloperMessageItemResource : ResponsesMessageItemResource
internal sealed class ResponsesDeveloperMessageItemResource : ResponsesMessageItemResource
{
/// <summary>
/// The constant role type identifier for developer messages.
@@ -131,13 +131,13 @@ internal sealed record ResponsesDeveloperMessageItemResource : ResponsesMessageI
/// The content of the message.
/// </summary>
[JsonPropertyName("content")]
public required IList<ItemContent> Content { get; init; }
public required List<ItemContent> Content { get; init; }
}
/// <summary>
/// A function tool call item resource.
/// </summary>
internal sealed record FunctionToolCallItemResource : ItemResource
internal sealed class FunctionToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for function call items.
@@ -175,7 +175,7 @@ internal sealed record FunctionToolCallItemResource : ItemResource
/// <summary>
/// A function tool call output item resource.
/// </summary>
internal sealed record FunctionToolCallOutputItemResource : ItemResource
internal sealed class FunctionToolCallOutputItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for function call output items.
@@ -208,7 +208,7 @@ internal sealed record FunctionToolCallOutputItemResource : ItemResource
/// The status of a message item resource.
/// </summary>
[JsonConverter(typeof(SnakeCaseEnumConverter<ResponsesMessageItemResourceStatus>))]
public enum ResponsesMessageItemResourceStatus
internal enum ResponsesMessageItemResourceStatus
{
/// <summary>
/// The message is completed.
@@ -230,7 +230,7 @@ public enum ResponsesMessageItemResourceStatus
/// The status of a function tool call item resource.
/// </summary>
[JsonConverter(typeof(SnakeCaseEnumConverter<FunctionToolCallItemResourceStatus>))]
public enum FunctionToolCallItemResourceStatus
internal enum FunctionToolCallItemResourceStatus
{
/// <summary>
/// The function call is completed.
@@ -247,7 +247,7 @@ public enum FunctionToolCallItemResourceStatus
/// The status of a function tool call output item resource.
/// </summary>
[JsonConverter(typeof(SnakeCaseEnumConverter<FunctionToolCallOutputItemResourceStatus>))]
public enum FunctionToolCallOutputItemResourceStatus
internal enum FunctionToolCallOutputItemResourceStatus
{
/// <summary>
/// The function call output is completed.
@@ -266,7 +266,7 @@ public enum FunctionToolCallOutputItemResourceStatus
[JsonDerivedType(typeof(ItemContentOutputText), "output_text")]
[JsonDerivedType(typeof(ItemContentOutputAudio), "output_audio")]
[JsonDerivedType(typeof(ItemContentRefusal), "refusal")]
internal abstract record ItemContent
internal abstract class ItemContent
{
/// <summary>
/// The type of the content.
@@ -285,7 +285,7 @@ internal abstract record ItemContent
/// <summary>
/// Text input content.
/// </summary>
internal sealed record ItemContentInputText : ItemContent
internal sealed class ItemContentInputText : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -301,7 +301,7 @@ internal sealed record ItemContentInputText : ItemContent
/// <summary>
/// Audio input content.
/// </summary>
internal sealed record ItemContentInputAudio : ItemContent
internal sealed class ItemContentInputAudio : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -323,7 +323,7 @@ internal sealed record ItemContentInputAudio : ItemContent
/// <summary>
/// Image input content.
/// </summary>
internal sealed record ItemContentInputImage : ItemContent
internal sealed class ItemContentInputImage : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -352,7 +352,7 @@ internal sealed record ItemContentInputImage : ItemContent
/// <summary>
/// File input content.
/// </summary>
internal sealed record ItemContentInputFile : ItemContent
internal sealed class ItemContentInputFile : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -380,7 +380,7 @@ internal sealed record ItemContentInputFile : ItemContent
/// <summary>
/// Text output content.
/// </summary>
internal sealed record ItemContentOutputText : ItemContent
internal sealed class ItemContentOutputText : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -396,19 +396,19 @@ internal sealed record ItemContentOutputText : ItemContent
/// The annotations.
/// </summary>
[JsonPropertyName("annotations")]
public required IList<JsonElement> Annotations { get; init; }
public required List<JsonElement> Annotations { get; init; }
/// <summary>
/// Log probability information for the output tokens.
/// </summary>
[JsonPropertyName("logprobs")]
public IList<JsonElement> Logprobs { get; init; } = [];
public List<JsonElement> Logprobs { get; init; } = [];
}
/// <summary>
/// Audio output content.
/// </summary>
internal sealed record ItemContentOutputAudio : ItemContent
internal sealed class ItemContentOutputAudio : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -430,7 +430,7 @@ internal sealed record ItemContentOutputAudio : ItemContent
/// <summary>
/// Refusal content.
/// </summary>
internal sealed record ItemContentRefusal : ItemContent
internal sealed class ItemContentRefusal : ItemContent
{
/// <inheritdoc/>
[JsonIgnore]
@@ -448,7 +448,7 @@ internal sealed record ItemContentRefusal : ItemContent
/// <summary>
/// A file search tool call item resource.
/// </summary>
internal sealed record FileSearchToolCallItemResource : ItemResource
internal sealed class FileSearchToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for file search call items.
@@ -463,12 +463,24 @@ internal sealed record FileSearchToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The queries used to search for files.
/// </summary>
[JsonPropertyName("queries")]
public List<string>? Queries { get; init; }
/// <summary>
/// The results of the file search tool call.
/// </summary>
[JsonPropertyName("results")]
public List<JsonElement>? Results { get; init; }
}
/// <summary>
/// A computer tool call item resource.
/// </summary>
internal sealed record ComputerToolCallItemResource : ItemResource
internal sealed class ComputerToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for computer call items.
@@ -483,12 +495,30 @@ internal sealed record ComputerToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// An identifier used when responding to the tool call with output.
/// </summary>
[JsonPropertyName("call_id")]
public string? CallId { get; init; }
/// <summary>
/// The action to perform.
/// </summary>
[JsonPropertyName("action")]
public JsonElement? Action { get; init; }
/// <summary>
/// The pending safety checks for the computer call.
/// </summary>
[JsonPropertyName("pending_safety_checks")]
public List<JsonElement>? PendingSafetyChecks { get; init; }
}
/// <summary>
/// A computer tool call output item resource.
/// </summary>
internal sealed record ComputerToolCallOutputItemResource : ItemResource
internal sealed class ComputerToolCallOutputItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for computer call output items.
@@ -503,12 +533,30 @@ internal sealed record ComputerToolCallOutputItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The ID of the computer tool call that produced the output.
/// </summary>
[JsonPropertyName("call_id")]
public string? CallId { get; init; }
/// <summary>
/// The safety checks reported by the API that have been acknowledged by the developer.
/// </summary>
[JsonPropertyName("acknowledged_safety_checks")]
public List<JsonElement>? AcknowledgedSafetyChecks { get; init; }
/// <summary>
/// The output of the computer tool call.
/// </summary>
[JsonPropertyName("output")]
public JsonElement? Output { get; init; }
}
/// <summary>
/// A web search tool call item resource.
/// </summary>
internal sealed record WebSearchToolCallItemResource : ItemResource
internal sealed class WebSearchToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for web search call items.
@@ -523,12 +571,18 @@ internal sealed record WebSearchToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// An object describing the specific action taken in this web search call.
/// </summary>
[JsonPropertyName("action")]
public JsonElement? Action { get; init; }
}
/// <summary>
/// A reasoning item resource.
/// </summary>
internal sealed record ReasoningItemResource : ItemResource
internal sealed class ReasoningItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for reasoning items.
@@ -543,12 +597,25 @@ internal sealed record ReasoningItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The encrypted content of the reasoning item - populated when a response is
/// generated with reasoning.encrypted_content in the include parameter.
/// </summary>
[JsonPropertyName("encrypted_content")]
public string? EncryptedContent { get; init; }
/// <summary>
/// Reasoning text contents.
/// </summary>
[JsonPropertyName("summary")]
public List<JsonElement>? Summary { get; init; }
}
/// <summary>
/// An item reference item resource.
/// </summary>
internal sealed record ItemReferenceItemResource : ItemResource
internal sealed class ItemReferenceItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for item reference items.
@@ -562,7 +629,7 @@ internal sealed record ItemReferenceItemResource : ItemResource
/// <summary>
/// An image generation tool call item resource.
/// </summary>
internal sealed record ImageGenerationToolCallItemResource : ItemResource
internal sealed class ImageGenerationToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for image generation call items.
@@ -577,12 +644,18 @@ internal sealed record ImageGenerationToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The generated image encoded in base64.
/// </summary>
[JsonPropertyName("result")]
public string? Result { get; init; }
}
/// <summary>
/// A code interpreter tool call item resource.
/// </summary>
internal sealed record CodeInterpreterToolCallItemResource : ItemResource
internal sealed class CodeInterpreterToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for code interpreter call items.
@@ -597,12 +670,31 @@ internal sealed record CodeInterpreterToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The ID of the container used to run the code.
/// </summary>
[JsonPropertyName("container_id")]
public string? ContainerId { get; init; }
/// <summary>
/// The code to run, or null if not available.
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; init; }
/// <summary>
/// The outputs generated by the code interpreter, such as logs or images.
/// Can be null if no outputs are available.
/// </summary>
[JsonPropertyName("outputs")]
public List<JsonElement>? Outputs { get; init; }
}
/// <summary>
/// A local shell tool call item resource.
/// </summary>
internal sealed record LocalShellToolCallItemResource : ItemResource
internal sealed class LocalShellToolCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for local shell call items.
@@ -617,12 +709,24 @@ internal sealed record LocalShellToolCallItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// The unique ID of the local shell tool call generated by the model.
/// </summary>
[JsonPropertyName("call_id")]
public string? CallId { get; init; }
/// <summary>
/// The action to execute.
/// </summary>
[JsonPropertyName("action")]
public JsonElement? Action { get; init; }
}
/// <summary>
/// A local shell tool call output item resource.
/// </summary>
internal sealed record LocalShellToolCallOutputItemResource : ItemResource
internal sealed class LocalShellToolCallOutputItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for local shell call output items.
@@ -637,12 +741,18 @@ internal sealed record LocalShellToolCallOutputItemResource : ItemResource
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; init; }
/// <summary>
/// A JSON string of the output of the local shell tool call.
/// </summary>
[JsonPropertyName("output")]
public string? Output { get; init; }
}
/// <summary>
/// An MCP list tools item resource.
/// </summary>
internal sealed record MCPListToolsItemResource : ItemResource
internal sealed class MCPListToolsItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for MCP list tools items.
@@ -651,12 +761,30 @@ internal sealed record MCPListToolsItemResource : ItemResource
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The tools available on the server.
/// </summary>
[JsonPropertyName("tools")]
public List<JsonElement>? Tools { get; init; }
/// <summary>
/// Error message if the server could not list tools.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
}
/// <summary>
/// An MCP approval request item resource.
/// </summary>
internal sealed record MCPApprovalRequestItemResource : ItemResource
internal sealed class MCPApprovalRequestItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for MCP approval request items.
@@ -665,12 +793,30 @@ internal sealed record MCPApprovalRequestItemResource : ItemResource
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server making the request.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The name of the tool to run.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// A JSON string of arguments for the tool.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; init; }
}
/// <summary>
/// An MCP approval response item resource.
/// </summary>
internal sealed record MCPApprovalResponseItemResource : ItemResource
internal sealed class MCPApprovalResponseItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for MCP approval response items.
@@ -679,12 +825,30 @@ internal sealed record MCPApprovalResponseItemResource : ItemResource
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The ID of the approval request being answered.
/// </summary>
[JsonPropertyName("approval_request_id")]
public string? ApprovalRequestId { get; init; }
/// <summary>
/// Whether the request was approved.
/// </summary>
[JsonPropertyName("approve")]
public bool? Approve { get; init; }
/// <summary>
/// Optional reason for the decision.
/// </summary>
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// An MCP call item resource.
/// </summary>
internal sealed record MCPCallItemResource : ItemResource
internal sealed class MCPCallItemResource : ItemResource
{
/// <summary>
/// The constant item type identifier for MCP call items.
@@ -693,4 +857,34 @@ internal sealed record MCPCallItemResource : ItemResource
/// <inheritdoc/>
public override string Type => ItemType;
/// <summary>
/// The label of the MCP server running the tool.
/// </summary>
[JsonPropertyName("server_label")]
public string? ServerLabel { get; init; }
/// <summary>
/// The name of the tool that was run.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// A JSON string of the arguments passed to the tool.
/// </summary>
[JsonPropertyName("arguments")]
public string? Arguments { get; init; }
/// <summary>
/// The output from the tool call.
/// </summary>
[JsonPropertyName("output")]
public string? Output { get; init; }
/// <summary>
/// The error from the tool call, if any.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; init; }
}
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Reference to a prompt template and its variables.
/// </summary>
internal sealed record PromptReference
internal sealed class PromptReference
{
/// <summary>
/// The ID of the prompt template to use.
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Configuration options for reasoning models.
/// </summary>
internal sealed record ReasoningOptions
internal sealed class ReasoningOptions
{
/// <summary>
/// Constrains effort on reasoning for reasoning models.
@@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// The status of a response generation.
/// </summary>
[JsonConverter(typeof(SnakeCaseEnumConverter<ResponseStatus>))]
public enum ResponseStatus
internal enum ResponseStatus
{
/// <summary>
/// The response has been completed.
@@ -111,7 +111,7 @@ internal sealed record Response
/// The output items (messages) generated in the response.
/// </summary>
[JsonPropertyName("output")]
public required IList<ItemResource> Output { get; init; }
public required List<ItemResource> Output { get; init; }
/// <summary>
/// A system (or developer) message inserted into the model's context.
@@ -135,7 +135,7 @@ internal sealed record Response
/// An array of tools the model may call while generating a response.
/// </summary>
[JsonPropertyName("tools")]
public required IList<JsonElement> Tools { get; init; }
public required List<JsonElement> Tools { get; init; }
/// <summary>
/// How the model should select which tool (or tools) to use when generating a response.
@@ -288,6 +288,9 @@ internal sealed record IncompleteDetails
/// </summary>
internal sealed record ResponseUsage
{
/// <summary>
/// Gets a zero usage instance.
/// </summary>
public static ResponseUsage Zero { get; } = new()
{
InputTokens = 0,
@@ -21,7 +21,7 @@ internal sealed class ResponseInput : IEquatable<ResponseInput>
this.Messages = null;
}
private ResponseInput(IReadOnlyList<InputMessage> messages)
private ResponseInput(List<InputMessage> messages)
{
this.Messages = messages ?? throw new ArgumentNullException(nameof(messages));
this.Text = null;
@@ -35,12 +35,12 @@ internal sealed class ResponseInput : IEquatable<ResponseInput>
/// <summary>
/// Creates a ResponseInput from a list of messages.
/// </summary>
public static ResponseInput FromMessages(IReadOnlyList<InputMessage> messages) => new(messages);
public static ResponseInput FromMessages(List<InputMessage> messages) => new(messages);
/// <summary>
/// Creates a ResponseInput from a list of messages.
/// </summary>
public static ResponseInput FromMessages(params InputMessage[] messages) => new(messages);
public static ResponseInput FromMessages(params InputMessage[] messages) => new(messages.ToList());
/// <summary>
/// Implicit conversion from string to ResponseInput.
@@ -75,12 +75,13 @@ internal sealed class ResponseInput : IEquatable<ResponseInput>
/// <summary>
/// Gets the messages value, or null if this is not a messages input.
/// </summary>
public IReadOnlyList<InputMessage>? Messages { get; }
public List<InputMessage>? Messages { get; }
/// <summary>
/// Gets the input as a list of InputMessage objects.
/// </summary>
public IReadOnlyList<InputMessage> GetInputMessages()
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Method performs transformation logic")]
public List<InputMessage> GetInputMessages()
{
if (this.Text is not null)
{
@@ -164,6 +165,7 @@ internal sealed class ResponseInput : IEquatable<ResponseInput>
/// </summary>
internal sealed class ResponseInputJsonConverter : JsonConverter<ResponseInput>
{
/// <inheritdoc/>
public override ResponseInput? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Check if it's a string
@@ -176,13 +178,14 @@ internal sealed class ResponseInputJsonConverter : JsonConverter<ResponseInput>
// Check if it's an array
if (reader.TokenType == JsonTokenType.StartArray)
{
var messages = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListInputMessage);
var messages = JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ListInputMessage);
return messages is not null ? ResponseInput.FromMessages(messages) : null;
}
throw new JsonException($"Unexpected token type for ResponseInput: {reader.TokenType}");
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ResponseInput value, JsonSerializerOptions options)
{
if (value.IsText)
@@ -191,7 +194,7 @@ internal sealed class ResponseInputJsonConverter : JsonConverter<ResponseInput>
}
else if (value.IsMessages)
{
JsonSerializer.Serialize(writer, value.Messages!, ResponsesJsonContext.Default.IReadOnlyListInputMessage);
JsonSerializer.Serialize(writer, value.Messages!, OpenAIHostingJsonContext.Default.ListInputMessage);
}
else
{
@@ -7,16 +7,8 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Options for streaming responses. Only set this when you set stream: true.
/// </summary>
internal sealed record StreamOptions
internal sealed class StreamOptions
{
/// <summary>
/// If set, an additional chunk will be streamed before the data: [DONE] message.
/// The usage field on this chunk shows the token usage statistics for the entire request,
/// and the choices field will always be an empty array.
/// </summary>
[JsonPropertyName("include_usage")]
public bool? IncludeUsage { get; init; }
/// <summary>
/// When true, stream obfuscation will be enabled. Stream obfuscation adds random characters
/// to an obfuscation field on streaming delta events to normalize payload sizes as a mitigation
@@ -16,6 +16,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
[JsonDerivedType(typeof(StreamingResponseCompleted), StreamingResponseCompleted.EventType)]
[JsonDerivedType(typeof(StreamingResponseIncomplete), StreamingResponseIncomplete.EventType)]
[JsonDerivedType(typeof(StreamingResponseFailed), StreamingResponseFailed.EventType)]
[JsonDerivedType(typeof(StreamingResponseCancelled), StreamingResponseCancelled.EventType)]
[JsonDerivedType(typeof(StreamingOutputItemAdded), StreamingOutputItemAdded.EventType)]
[JsonDerivedType(typeof(StreamingOutputItemDone), StreamingOutputItemDone.EventType)]
[JsonDerivedType(typeof(StreamingContentPartAdded), StreamingContentPartAdded.EventType)]
@@ -26,7 +27,10 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
[JsonDerivedType(typeof(StreamingFunctionCallArgumentsDone), StreamingFunctionCallArgumentsDone.EventType)]
[JsonDerivedType(typeof(StreamingReasoningSummaryTextDelta), StreamingReasoningSummaryTextDelta.EventType)]
[JsonDerivedType(typeof(StreamingReasoningSummaryTextDone), StreamingReasoningSummaryTextDone.EventType)]
internal abstract record StreamingResponseEvent
[JsonDerivedType(typeof(StreamingWorkflowEventComplete), StreamingWorkflowEventComplete.EventType)]
[JsonDerivedType(typeof(StreamingFunctionApprovalRequested), StreamingFunctionApprovalRequested.EventType)]
[JsonDerivedType(typeof(StreamingFunctionApprovalResponded), StreamingFunctionApprovalResponded.EventType)]
internal abstract class StreamingResponseEvent
{
/// <summary>
/// Gets the type identifier for the streaming response event.
@@ -43,11 +47,22 @@ internal abstract record StreamingResponseEvent
public int SequenceNumber { get; init; }
}
/// <summary>
/// Denotes an <see cref="StreamingResponseEvent"/> instance which contains an update to the <see cref="Models.Response"/> instance.
/// </summary>
internal interface IStreamingResponseEventWithResponse
{
/// <summary>
/// Gets the response object associated with this streaming event.
/// </summary>
Response Response { get; }
}
/// <summary>
/// Represents a streaming response event indicating that a new response has been created and streaming has begun.
/// This is typically the first event sent in a streaming response sequence.
/// </summary>
internal sealed record StreamingResponseCreated : StreamingResponseEvent
internal sealed class StreamingResponseCreated : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response created events.
@@ -69,7 +84,7 @@ internal sealed record StreamingResponseCreated : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event indicating that the response is in progress.
/// </summary>
internal sealed record StreamingResponseInProgress : StreamingResponseEvent
internal sealed class StreamingResponseInProgress : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response in progress events.
@@ -91,7 +106,7 @@ internal sealed record StreamingResponseInProgress : StreamingResponseEvent
/// Represents a streaming response event indicating that the response has been completed.
/// This is typically the last event sent in a streaming response sequence.
/// </summary>
internal sealed record StreamingResponseCompleted : StreamingResponseEvent
internal sealed class StreamingResponseCompleted : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response completed events.
@@ -113,7 +128,7 @@ internal sealed record StreamingResponseCompleted : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event indicating that the response finished as incomplete.
/// </summary>
internal sealed record StreamingResponseIncomplete : StreamingResponseEvent
internal sealed class StreamingResponseIncomplete : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response incomplete events.
@@ -134,7 +149,7 @@ internal sealed record StreamingResponseIncomplete : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event indicating that the response has failed.
/// </summary>
internal sealed record StreamingResponseFailed : StreamingResponseEvent
internal sealed class StreamingResponseFailed : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response failed events.
@@ -152,11 +167,33 @@ internal sealed record StreamingResponseFailed : StreamingResponseEvent
public required Response Response { get; init; }
}
/// <summary>
/// Represents a streaming response event indicating that the response has been cancelled.
/// Only responses created with background=true can be cancelled.
/// </summary>
internal sealed class StreamingResponseCancelled : StreamingResponseEvent, IStreamingResponseEventWithResponse
{
/// <summary>
/// The constant event type identifier for response cancelled events.
/// </summary>
public const string EventType = "response.cancelled";
/// <inheritdoc/>
[JsonIgnore]
public override string Type => EventType;
/// <summary>
/// Gets or sets the cancelled response object.
/// </summary>
[JsonPropertyName("response")]
public required Response Response { get; init; }
}
/// <summary>
/// Represents a streaming response event indicating that a new output item has been added to the response.
/// This event is sent when the AI agent produces a new piece of content during streaming.
/// </summary>
internal sealed record StreamingOutputItemAdded : StreamingResponseEvent
internal sealed class StreamingOutputItemAdded : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for output item added events.
@@ -186,7 +223,7 @@ internal sealed record StreamingOutputItemAdded : StreamingResponseEvent
/// Represents a streaming response event indicating that an output item has been completed.
/// This event is sent when the AI agent finishes producing a particular piece of content.
/// </summary>
internal sealed record StreamingOutputItemDone : StreamingResponseEvent
internal sealed class StreamingOutputItemDone : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for output item done events.
@@ -215,7 +252,7 @@ internal sealed record StreamingOutputItemDone : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event indicating that a new content part has been added to an output item.
/// </summary>
internal sealed record StreamingContentPartAdded : StreamingResponseEvent
internal sealed class StreamingContentPartAdded : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for content part added events.
@@ -254,7 +291,7 @@ internal sealed record StreamingContentPartAdded : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event indicating that a content part has been completed.
/// </summary>
internal sealed record StreamingContentPartDone : StreamingResponseEvent
internal sealed class StreamingContentPartDone : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for content part done events.
@@ -293,7 +330,7 @@ internal sealed record StreamingContentPartDone : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event containing a text delta (incremental text chunk).
/// </summary>
internal sealed record StreamingOutputTextDelta : StreamingResponseEvent
internal sealed class StreamingOutputTextDelta : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for output text delta events.
@@ -332,13 +369,13 @@ internal sealed record StreamingOutputTextDelta : StreamingResponseEvent
/// Gets or sets the log probability information for the output tokens.
/// </summary>
[JsonPropertyName("logprobs")]
public IList<JsonElement> Logprobs { get; init; } = [];
public List<JsonElement> Logprobs { get; init; } = [];
}
/// <summary>
/// Represents a streaming response event indicating that output text has been completed.
/// </summary>
internal sealed record StreamingOutputTextDone : StreamingResponseEvent
internal sealed class StreamingOutputTextDone : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for output text done events.
@@ -377,7 +414,7 @@ internal sealed record StreamingOutputTextDone : StreamingResponseEvent
/// <summary>
/// Represents a streaming response event containing a function call arguments delta.
/// </summary>
internal sealed record StreamingFunctionCallArgumentsDelta : StreamingResponseEvent
internal sealed class StreamingFunctionCallArgumentsDelta : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for function call arguments delta events.
@@ -410,7 +447,7 @@ internal sealed record StreamingFunctionCallArgumentsDelta : StreamingResponseEv
/// <summary>
/// Represents a streaming response event indicating that function call arguments are complete.
/// </summary>
internal sealed record StreamingFunctionCallArgumentsDone : StreamingResponseEvent
internal sealed class StreamingFunctionCallArgumentsDone : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for function call arguments done events.
@@ -443,7 +480,7 @@ internal sealed record StreamingFunctionCallArgumentsDone : StreamingResponseEve
/// <summary>
/// Represents a streaming response event containing a reasoning summary text delta (incremental text chunk).
/// </summary>
internal sealed record StreamingReasoningSummaryTextDelta : StreamingResponseEvent
internal sealed class StreamingReasoningSummaryTextDelta : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for reasoning summary text delta events.
@@ -482,7 +519,7 @@ internal sealed record StreamingReasoningSummaryTextDelta : StreamingResponseEve
/// <summary>
/// Represents a streaming response event indicating that reasoning summary text has been completed.
/// </summary>
internal sealed record StreamingReasoningSummaryTextDone : StreamingResponseEvent
internal sealed class StreamingReasoningSummaryTextDone : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for reasoning summary text done events.
@@ -517,3 +554,148 @@ internal sealed record StreamingReasoningSummaryTextDone : StreamingResponseEven
[JsonPropertyName("text")]
public required string Text { get; init; }
}
/// <summary>
/// Represents a streaming response event containing a workflow event.
/// This event is sent during workflow execution to provide observability into workflow steps,
/// executor invocations, errors, and other workflow lifecycle events.
/// </summary>
internal sealed class StreamingWorkflowEventComplete : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for workflow event events.
/// </summary>
public const string EventType = "response.workflow_event.complete";
/// <inheritdoc/>
[JsonIgnore]
public override string Type => EventType;
/// <summary>
/// Gets or sets the index of the output in the response.
/// </summary>
[JsonPropertyName("output_index")]
public int OutputIndex { get; set; }
/// <summary>
/// Gets or sets the workflow event data containing event type, executor ID, and event-specific data.
/// </summary>
[JsonPropertyName("data")]
public JsonElement? Data { get; set; }
/// <summary>
/// Gets or sets the executor ID if this is an executor-scoped event.
/// </summary>
[JsonPropertyName("executor_id")]
public string? ExecutorId { get; set; }
/// <summary>
/// Gets or sets the item ID for tracking purposes.
/// </summary>
[JsonPropertyName("item_id")]
public string? ItemId { get; set; }
}
/// <summary>
/// Represents a streaming response event indicating a function approval has been requested.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class StreamingFunctionApprovalRequested : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for function approval requested events.
/// </summary>
public const string EventType = "response.function_approval.requested";
/// <inheritdoc/>
[JsonIgnore]
public override string Type => EventType;
/// <summary>
/// Gets or sets the unique identifier for the approval request.
/// </summary>
[JsonPropertyName("request_id")]
public required string RequestId { get; init; }
/// <summary>
/// Gets or sets the function call that requires approval.
/// </summary>
[JsonPropertyName("function_call")]
public required FunctionCallInfo FunctionCall { get; init; }
/// <summary>
/// Gets or sets the item ID for tracking purposes.
/// </summary>
[JsonPropertyName("item_id")]
public required string ItemId { get; init; }
/// <summary>
/// Gets or sets the output index.
/// </summary>
[JsonPropertyName("output_index")]
public int OutputIndex { get; init; }
}
/// <summary>
/// Represents a streaming response event indicating a function approval has been responded to.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class StreamingFunctionApprovalResponded : StreamingResponseEvent
{
/// <summary>
/// The constant event type identifier for function approval responded events.
/// </summary>
public const string EventType = "response.function_approval.responded";
/// <inheritdoc/>
[JsonIgnore]
public override string Type => EventType;
/// <summary>
/// Gets or sets the unique identifier of the approval request being responded to.
/// </summary>
[JsonPropertyName("request_id")]
public required string RequestId { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the function call was approved.
/// </summary>
[JsonPropertyName("approved")]
public bool Approved { get; init; }
/// <summary>
/// Gets or sets the item ID for tracking purposes.
/// </summary>
[JsonPropertyName("item_id")]
public required string ItemId { get; init; }
/// <summary>
/// Gets or sets the output index.
/// </summary>
[JsonPropertyName("output_index")]
public int OutputIndex { get; init; }
}
/// <summary>
/// Represents function call information for approval events.
/// </summary>
internal sealed class FunctionCallInfo
{
/// <summary>
/// Gets or sets the function call ID.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Gets or sets the function name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Gets or sets the function arguments.
/// </summary>
[JsonPropertyName("arguments")]
public required JsonElement Arguments { get; init; }
}
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Configuration options for a text response from the model.
/// </summary>
internal sealed record TextConfiguration
internal sealed class TextConfiguration
{
/// <summary>
/// The format configuration for the text response.
@@ -34,7 +34,7 @@ internal sealed record TextConfiguration
[JsonDerivedType(typeof(ResponseTextFormatConfigurationText), "text")]
[JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonObject), "json_object")]
[JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonSchema), "json_schema")]
internal abstract record ResponseTextFormatConfiguration
internal abstract class ResponseTextFormatConfiguration
{
/// <summary>
/// The type of response format.
@@ -46,7 +46,7 @@ internal abstract record ResponseTextFormatConfiguration
/// <summary>
/// Plain text response format configuration.
/// </summary>
internal sealed record ResponseTextFormatConfigurationText : ResponseTextFormatConfiguration
internal sealed class ResponseTextFormatConfigurationText : ResponseTextFormatConfiguration
{
/// <summary>
/// Gets the type of response format. Always "text".
@@ -59,7 +59,7 @@ internal sealed record ResponseTextFormatConfigurationText : ResponseTextFormatC
/// JSON object response format configuration.
/// Ensures the message the model generates is valid JSON.
/// </summary>
internal sealed record ResponseTextFormatConfigurationJsonObject : ResponseTextFormatConfiguration
internal sealed class ResponseTextFormatConfigurationJsonObject : ResponseTextFormatConfiguration
{
/// <summary>
/// Gets the type of response format. Always "json_object".
@@ -71,7 +71,7 @@ internal sealed record ResponseTextFormatConfigurationJsonObject : ResponseTextF
/// <summary>
/// JSON schema response format configuration with structured output schema.
/// </summary>
internal sealed record ResponseTextFormatConfigurationJsonSchema : ResponseTextFormatConfiguration
internal sealed class ResponseTextFormatConfigurationJsonSchema : ResponseTextFormatConfiguration
{
/// <summary>
/// Gets the type of response format. Always "json_schema".
@@ -97,7 +97,7 @@ internal sealed record ResponseTextFormatConfigurationJsonSchema : ResponseTextF
/// The JSON schema for structured outputs.
/// </summary>
[JsonPropertyName("schema")]
public required Dictionary<string, object> Schema { get; init; }
public required JsonElement Schema { get; init; }
/// <summary>
/// Whether to enable strict schema adherence when generating the output.
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
/// <summary>
/// Represents workflow event data for serialization.
/// </summary>
internal sealed class WorkflowEventData
{
/// <summary>
/// The type of the workflow event.
/// </summary>
[JsonPropertyName("event_type")]
public required string EventType { get; init; }
/// <summary>
/// The event data payload.
/// </summary>
[JsonPropertyName("data")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonElement? Data { get; init; }
/// <summary>
/// The executor ID, if this is an executor event.
/// </summary>
[JsonPropertyName("executor_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ExecutorId { get; init; }
/// <summary>
/// The timestamp when the event occurred.
/// </summary>
[JsonPropertyName("timestamp")]
public required string Timestamp { get; init; }
}
@@ -0,0 +1,227 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Handles route requests for OpenAI Responses API endpoints.
/// </summary>
internal sealed class ResponsesHttpHandler
{
private readonly IResponsesService _responsesService;
/// <summary>
/// Initializes a new instance of the <see cref="ResponsesHttpHandler"/> class.
/// </summary>
/// <param name="responsesService">The responses service.</param>
public ResponsesHttpHandler(IResponsesService responsesService)
{
this._responsesService = responsesService ?? throw new ArgumentNullException(nameof(responsesService));
}
/// <summary>
/// Creates a model response for the given input.
/// </summary>
public async Task<IResult> CreateResponseAsync(
[FromBody] CreateResponse request,
[FromQuery] bool? stream,
CancellationToken cancellationToken)
{
try
{
// Handle streaming vs non-streaming
bool shouldStream = stream ?? request.Stream ?? false;
if (shouldStream)
{
var streamingResponse = this._responsesService.CreateResponseStreamingAsync(
request,
cancellationToken: cancellationToken);
return new SseJsonResult<StreamingResponseEvent>(
streamingResponse,
static evt => evt.Type,
OpenAIHostingJsonContext.Default.StreamingResponseEvent);
}
var response = await this._responsesService.CreateResponseAsync(
request,
cancellationToken: cancellationToken).ConfigureAwait(false);
return Results.Ok(response);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Mutually exclusive"))
{
// Return OpenAI-style error for mutual exclusivity violations
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error",
Code = "mutually_exclusive_parameters"
}
});
}
catch (InvalidOperationException ex) when (ex.Message.Contains("not found") || ex.Message.Contains("does not exist"))
{
// Return OpenAI-style error for not found errors
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error"
}
});
}
catch (InvalidOperationException ex) when (ex.Message.Contains("No 'agent.name' or 'model' specified"))
{
// Return OpenAI-style error for missing required parameters
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error",
Code = "missing_required_parameter"
}
});
}
}
/// <summary>
/// Retrieves a response by ID.
/// </summary>
public async Task<IResult> GetResponseAsync(
string responseId,
[FromQuery] string[]? include,
[FromQuery] bool? stream,
[FromQuery] int? starting_after,
CancellationToken cancellationToken)
{
// If streaming is requested, return SSE stream
if (stream == true)
{
var streamingResponse = this._responsesService.GetResponseStreamingAsync(
responseId,
startingAfter: starting_after,
cancellationToken: cancellationToken);
return new SseJsonResult<StreamingResponseEvent>(
streamingResponse,
static evt => evt.Type,
OpenAIHostingJsonContext.Default.StreamingResponseEvent);
}
// Non-streaming: return the response object
var response = await this._responsesService.GetResponseAsync(responseId, cancellationToken).ConfigureAwait(false);
return response is not null
? Results.Ok(response)
: Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Response '{responseId}' not found.",
Type = "invalid_request_error"
}
});
}
/// <summary>
/// Cancels an in-progress response.
/// </summary>
public async Task<IResult> CancelResponseAsync(
string responseId,
CancellationToken cancellationToken)
{
try
{
var response = await this._responsesService.CancelResponseAsync(responseId, cancellationToken).ConfigureAwait(false);
return Results.Ok(response);
}
catch (InvalidOperationException ex)
{
return Results.BadRequest(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error"
}
});
}
}
/// <summary>
/// Deletes a response.
/// </summary>
public async Task<IResult> DeleteResponseAsync(
string responseId,
CancellationToken cancellationToken)
{
var deleted = await this._responsesService.DeleteResponseAsync(responseId, cancellationToken).ConfigureAwait(false);
return deleted
? Results.Ok(new DeleteResponse { Id = responseId, Object = "response", Deleted = true })
: Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = $"Response '{responseId}' not found.",
Type = "invalid_request_error"
}
});
}
/// <summary>
/// Lists the input items for a response.
/// </summary>
public async Task<IResult> ListResponseInputItemsAsync(
string responseId,
[FromQuery] int? limit,
[FromQuery] string? order,
[FromQuery] string? after,
[FromQuery] string? before,
CancellationToken cancellationToken)
{
try
{
// Convert string order to SortOrder enum
SortOrder? sortOrder = order switch
{
string s when s.Equals("asc", StringComparison.OrdinalIgnoreCase) => SortOrder.Ascending,
string s when s.Equals("desc", StringComparison.OrdinalIgnoreCase) => SortOrder.Descending,
null => null,
_ => throw new InvalidOperationException($"Invalid order value: {order}. Must be 'asc' or 'desc'.")
};
var result = await this._responsesService.ListResponseInputItemsAsync(
responseId,
limit,
sortOrder,
after,
before,
cancellationToken).ConfigureAwait(false);
return Results.Ok(result);
}
catch (InvalidOperationException ex)
{
return Results.NotFound(new ErrorResponse
{
Error = new ErrorDetails
{
Message = ex.Message,
Type = "invalid_request_error"
}
});
}
}
}
@@ -1,24 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
/// <summary>
/// Extension methods for JSON serialization.
/// </summary>
internal static class ResponsesJsonSerializerOptions
{
/// <summary>
/// Gets the default JSON serializer options.
/// </summary>
public static JsonSerializerOptions Default { get; } = Create();
private static JsonSerializerOptions Create()
{
JsonSerializerOptions options = new(ResponsesJsonContext.Default.Options);
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.MakeReadOnly();
return options;
}
}
@@ -16,27 +16,18 @@ internal sealed class AudioContentEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) =>
content is DataContent dataContent && dataContent.HasTopLevelMediaType("audio");
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not DataContent audioData || !audioData.HasTopLevelMediaType("audio"))
{
throw new InvalidOperationException("AudioContentEventGenerator only supports audio DataContent.");
}
var itemId = idGenerator.GenerateMessageId();
var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputAudio;
if (itemContent == null)
if (ItemContentConverter.ToItemContent(content) is not ItemContentInputAudio itemContent)
{
throw new InvalidOperationException("Failed to convert audio content to ItemContentInputAudio.");
}
@@ -79,13 +70,7 @@ internal sealed class AudioContentEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -16,26 +16,17 @@ internal sealed class ErrorContentEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) => content is ErrorContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not ErrorContent)
{
throw new InvalidOperationException("ErrorContentEventGenerator only supports ErrorContent.");
}
var itemId = idGenerator.GenerateMessageId();
var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentRefusal;
if (itemContent == null)
if (ItemContentConverter.ToItemContent(content) is not ItemContentRefusal itemContent)
{
throw new InvalidOperationException("Failed to convert error content to ItemContentRefusal.");
}
@@ -78,13 +69,7 @@ internal sealed class ErrorContentEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -16,8 +16,6 @@ internal sealed class FileContentEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) =>
content is DataContent dataContent &&
!dataContent.HasTopLevelMediaType("image") &&
@@ -25,11 +23,6 @@ internal sealed class FileContentEventGenerator(
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not DataContent fileData ||
fileData.HasTopLevelMediaType("image") ||
fileData.HasTopLevelMediaType("audio"))
@@ -38,9 +31,7 @@ internal sealed class FileContentEventGenerator(
}
var itemId = idGenerator.GenerateMessageId();
var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile;
if (itemContent == null)
if (ItemContentConverter.ToItemContent(content) is not ItemContentInputFile itemContent)
{
throw new InvalidOperationException("Failed to convert file content to ItemContentInputFile.");
}
@@ -83,13 +74,7 @@ internal sealed class FileContentEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
/// <summary>
/// A generator for streaming events from function approval request content.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class FunctionApprovalRequestEventGenerator(
IdGenerator idGenerator,
SequenceNumber seq,
int outputIndex,
JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator
{
public override bool IsSupported(AIContent content) => content is FunctionApprovalRequestContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (content is not FunctionApprovalRequestContent approvalRequest)
{
throw new InvalidOperationException("FunctionApprovalRequestEventGenerator only supports FunctionApprovalRequestContent.");
}
yield return new StreamingFunctionApprovalRequested
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
RequestId = approvalRequest.Id,
ItemId = idGenerator.GenerateMessageId(),
FunctionCall = new FunctionCallInfo
{
Id = approvalRequest.FunctionCall.CallId,
Name = approvalRequest.FunctionCall.Name,
Arguments = JsonSerializer.SerializeToElement(
approvalRequest.FunctionCall.Arguments,
jsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object>)))
}
};
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
/// <summary>
/// A generator for streaming events from function approval response content.
/// This is a non-standard DevUI extension for human-in-the-loop scenarios.
/// </summary>
internal sealed class FunctionApprovalResponseEventGenerator(
IdGenerator idGenerator,
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
public override bool IsSupported(AIContent content) => content is FunctionApprovalResponseContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (content is not FunctionApprovalResponseContent approvalResponse)
{
throw new InvalidOperationException("FunctionApprovalResponseEventGenerator only supports FunctionApprovalResponseContent.");
}
yield return new StreamingFunctionApprovalResponded
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
RequestId = approvalResponse.Id,
Approved = approvalResponse.Approved,
ItemId = idGenerator.GenerateMessageId()
};
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -17,17 +17,10 @@ internal sealed class FunctionCallEventGenerator(
int outputIndex,
JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) => content is FunctionCallContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not FunctionCallContent functionCallContent)
{
throw new InvalidOperationException("FunctionCallEventGenerator only supports FunctionCallContent.");
@@ -63,13 +56,7 @@ internal sealed class FunctionCallEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -15,17 +15,10 @@ internal sealed class FunctionResultEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) => content is FunctionResultContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not FunctionResultContent functionResultContent)
{
throw new InvalidOperationException("FunctionResultEventGenerator only supports FunctionResultContent.");
@@ -45,13 +38,7 @@ internal sealed class FunctionResultEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -16,26 +16,17 @@ internal sealed class HostedFileContentEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) => content is HostedFileContent;
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
if (content is not HostedFileContent)
{
throw new InvalidOperationException("HostedFileContentEventGenerator only supports HostedFileContent.");
}
var itemId = idGenerator.GenerateMessageId();
var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile;
if (itemContent == null)
if (ItemContentConverter.ToItemContent(content) is not ItemContentInputFile itemContent)
{
throw new InvalidOperationException("Failed to convert hosted file content to ItemContentInputFile.");
}
@@ -78,13 +69,7 @@ internal sealed class HostedFileContentEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -16,22 +16,13 @@ internal sealed class ImageContentEventGenerator(
SequenceNumber seq,
int outputIndex) : StreamingEventGenerator
{
private bool _isCompleted;
public override bool IsSupported(AIContent content) =>
content is UriContent uriContent && uriContent.HasTopLevelMediaType("image") ||
content is DataContent dataContent && dataContent.HasTopLevelMediaType("image");
(content is UriContent uriContent && uriContent.HasTopLevelMediaType("image")) ||
(content is DataContent dataContent && dataContent.HasTopLevelMediaType("image"));
public override IEnumerable<StreamingResponseEvent> ProcessContent(AIContent content)
{
if (this._isCompleted)
{
throw new InvalidOperationException("Cannot process content after the generator has been completed.");
}
ItemContentInputImage? itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputImage;
if (itemContent == null)
if (ItemContentConverter.ToItemContent(content) is not ItemContentInputImage itemContent)
{
throw new InvalidOperationException("ImageContentEventGenerator only supports image UriContent and DataContent.");
}
@@ -76,13 +67,7 @@ internal sealed class ImageContentEventGenerator(
OutputIndex = outputIndex,
Item = item
};
this._isCompleted = true;
}
public override IEnumerable<StreamingResponseEvent> Complete()
{
this._isCompleted = true;
return [];
}
public override IEnumerable<StreamingResponseEvent> Complete() => [];
}
@@ -18,7 +18,7 @@ internal sealed class TextReasoningContentEventGenerator(
int outputIndex) : StreamingEventGenerator
{
private State _currentState = State.Initial;
private readonly string _itemId = idGenerator.GenerateMessageId();
private readonly string _itemId = idGenerator.GenerateReasoningId();
private readonly StringBuilder _text = new();
private const int SummaryIndex = 0; // Summary index for reasoning summary text
@@ -2,9 +2,12 @@
using System;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.OpenAI;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
using Microsoft.Agents.AI.Hosting.OpenAI.Conversations;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection;
@@ -29,6 +32,7 @@ public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions
/// <summary>
/// Adds support for exposing <see cref="AIAgent"/> instances via OpenAI Responses.
/// Uses the in-memory responses service implementation.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>The <see cref="IServiceCollection"/> for method chaining.</returns>
@@ -36,8 +40,39 @@ public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions
{
ArgumentNullException.ThrowIfNull(services);
services.Configure<JsonOptions>(options => options.SerializerOptions.TypeInfoResolverChain.Add(ResponsesJsonSerializerOptions.Default.TypeInfoResolver!));
services.Configure<JsonOptions>(options
=> options.SerializerOptions.TypeInfoResolverChain.Add(
OpenAIHostingJsonContext.Default.Options.TypeInfoResolver!));
services.TryAddSingleton<IConversationStorage, InMemoryConversationStorage>();
services.TryAddSingleton<IAgentConversationIndex, InMemoryAgentConversationIndex>();
services.TryAddSingleton<InMemoryStorageOptions>();
services.TryAddSingleton<IResponsesService>(sp =>
{
var executor = sp.GetRequiredService<IResponseExecutor>();
var options = sp.GetRequiredService<InMemoryStorageOptions>();
var conversationStorage = sp.GetService<IConversationStorage>();
return new InMemoryResponsesService(executor, options, conversationStorage);
});
services.TryAddSingleton<IResponseExecutor, HostedAgentResponseExecutor>();
return services;
}
/// <summary>
/// Adds in-memory conversation storage and indexing services to the service collection.
/// This is suitable only for development and testing scenarios.
/// </summary>
/// <param name="services">The service collection to add services to.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddOpenAIConversations(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// Register storage options
services.TryAddSingleton<InMemoryStorageOptions>();
services.TryAddSingleton<IConversationStorage, InMemoryConversationStorage>();
services.TryAddSingleton<IAgentConversationIndex, InMemoryAgentConversationIndex>();
return services;
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.ServerSentEvents;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.Agents.AI.Hosting.OpenAI;
/// <summary>
/// IResult implementation for streaming JSON data using Server-Sent Events (SSE).
/// </summary>
/// <typeparam name="T">The type of items to stream.</typeparam>
internal sealed class SseJsonResult<T> : IResult
{
private readonly IAsyncEnumerable<T> _events;
private readonly JsonTypeInfo<T> _jsonTypeInfo;
private readonly Func<T, string?> _getEventType;
/// <summary>
/// Initializes a new instance of the <see cref="SseJsonResult{T}"/> class.
/// </summary>
/// <param name="events">The async enumerable of items to stream.</param>
/// <param name="getEventType">A function to get the optional event type from each item.</param>
/// <param name="jsonTypeInfo">The JSON type information for serializing items.</param>
public SseJsonResult(IAsyncEnumerable<T> events, Func<T, string?> getEventType, JsonTypeInfo<T> jsonTypeInfo)
{
this._events = events ?? throw new ArgumentNullException(nameof(events));
this._jsonTypeInfo = jsonTypeInfo ?? throw new ArgumentNullException(nameof(jsonTypeInfo));
this._getEventType = getEventType ?? throw new ArgumentNullException(nameof(getEventType));
}
/// <summary>
/// Executes the result by streaming items to the HTTP response using Server-Sent Events format.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
public async Task ExecuteAsync(HttpContext httpContext)
{
var response = httpContext.Response;
var cancellationToken = httpContext.RequestAborted;
// Set SSE headers
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache,no-store";
response.Headers.Connection = "keep-alive";
response.Headers.ContentEncoding = "identity";
httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();
await SseFormatter.WriteAsync(
source: this.GetItemsAsync(),
destination: response.Body,
itemFormatter: this.FormatItem,
cancellationToken).ConfigureAwait(false);
}
private async IAsyncEnumerable<SseItem<T>> GetItemsAsync()
{
await foreach (var item in this._events.ConfigureAwait(false))
{
yield return new SseItem<T>(item, this._getEventType(item));
}
}
private void FormatItem(SseItem<T> sseItem, IBufferWriter<byte> bufferWriter)
{
using var writer = new Utf8JsonWriter(bufferWriter);
JsonSerializer.Serialize(writer, sseItem.Data, this._jsonTypeInfo);
writer.Flush();
}
}
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Unit tests for AgentInvocationContext.
/// </summary>
public sealed class AgentInvocationContextTests
{
[Fact]
public void Constructor_WithIdGenerator_InitializesCorrectly()
{
// Arrange
var idGenerator = new IdGenerator("resp_test123", "conv_test456");
// Act
var context = new AgentInvocationContext(idGenerator);
// Assert
Assert.NotNull(context);
Assert.Same(idGenerator, context.IdGenerator);
Assert.Equal("resp_test123", context.ResponseId);
Assert.Equal("conv_test456", context.ConversationId);
Assert.NotNull(context.JsonSerializerOptions);
}
[Fact]
public void Constructor_WithoutJsonOptions_UsesDefaultOptions()
{
// Arrange
var idGenerator = new IdGenerator("resp_test", "conv_test");
// Act
var context = new AgentInvocationContext(idGenerator);
// Assert
Assert.NotNull(context.JsonSerializerOptions);
Assert.Same(OpenAIHostingJsonUtilities.DefaultOptions, context.JsonSerializerOptions);
}
[Fact]
public void Constructor_WithCustomJsonOptions_UsesProvidedOptions()
{
// Arrange
var idGenerator = new IdGenerator("resp_test", "conv_test");
var customOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Act
var context = new AgentInvocationContext(idGenerator, customOptions);
// Assert
Assert.Same(customOptions, context.JsonSerializerOptions);
}
[Fact]
public void ResponseId_ReturnsIdGeneratorResponseId()
{
// Arrange
const string ResponseId = "resp_property_test";
var idGenerator = new IdGenerator(ResponseId, "conv_test");
var context = new AgentInvocationContext(idGenerator);
// Act
string result = context.ResponseId;
// Assert
Assert.Equal(ResponseId, result);
Assert.Equal(idGenerator.ResponseId, result);
}
[Fact]
public void ConversationId_ReturnsIdGeneratorConversationId()
{
// Arrange
const string ConversationId = "conv_property_test";
var idGenerator = new IdGenerator("resp_test", ConversationId);
var context = new AgentInvocationContext(idGenerator);
// Act
string result = context.ConversationId;
// Assert
Assert.Equal(ConversationId, result);
Assert.Equal(idGenerator.ConversationId, result);
}
}
@@ -226,6 +226,37 @@ public abstract class ConformanceTestBase : IAsyncDisposable
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIChatCompletions(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
this._httpClient = testServer.CreateClient();
return this._httpClient;
}
/// <summary>
/// Creates a test server with a mock chat client that returns function call content.
/// </summary>
protected async Task<HttpClient> CreateTestServerWithToolCallAsync(
string agentName,
string instructions,
string functionName,
string arguments)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.ToolCallMockChatClient(functionName, arguments);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
builder.AddOpenAIChatCompletions();
this._app = builder.Build();
@@ -0,0 +1,24 @@
{
"items": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is the weather like today?"
}
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Tell me a joke!"
}
]
}
]
}
@@ -0,0 +1,32 @@
{
"object": "list",
"data": [
{
"id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "What is the weather like today?"
}
],
"role": "user"
},
{
"id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822",
"type": "message",
"status": "completed",
"content": [
{
"type": "input_text",
"text": "Tell me a joke!"
}
],
"role": "user"
}
],
"first_id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"has_more": false,
"last_id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822"
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "basic_conversation"
}
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"object": "conversation",
"created_at": 1761318654,
"metadata": {
"test_type": "basic_conversation"
}
}
@@ -0,0 +1,6 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"input": "What is the capital of France?",
"max_output_tokens": 100
}
@@ -0,0 +1,70 @@
{
"id": "resp_04cbf451511948220068fb97bdec548195a367870aa85734de",
"object": "response",
"created_at": 1761318846,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 100,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_04cbf451511948220068fb97c0162881958d80862a0d253a14",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The capital of France is Paris."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 36,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 8,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 44
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,6 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822",
"input": "What is its population?",
"max_output_tokens": 150
}
@@ -0,0 +1,70 @@
{
"id": "resp_04cbf451511948220068fb97cf320881958b69530fe07eb2a9",
"object": "response",
"created_at": 1761318863,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 150,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the larger metropolitan area has a population of around 12 million. These numbers can vary, so it's always a good idea to check for the most recent statistics."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 56,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 58,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 114
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,624 @@
event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"in_progress","content":[],"role":"assistant"}}
event: response.content_part.added
data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"In","logprobs":[],"obfuscation":"C16oYk8aI5VtGp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"vXmOvISW7QRUF1"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"qEkC6mYZmi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" workshop","logprobs":[],"obfuscation":"2aAdNXN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" at","logprobs":[],"obfuscation":"bv66grEvpSema"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fVOKa91q3jxh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" edge","logprobs":[],"obfuscation":"kW1rIr6ZZBc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"RnPLx5DWhJvWO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"DMVs96dHxVd7fh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bustling","logprobs":[],"obfuscation":"9TCmdGs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"E4p2Nj5KH0Z"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lived","logprobs":[],"obfuscation":"e3kqeTLJpR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"zQmSxD9MrnbNr7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" curious","logprobs":[],"obfuscation":"wQHxX2wm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robot","logprobs":[],"obfuscation":"i49v38s1iB"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" named","logprobs":[],"obfuscation":"FC4nhPH5iI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"WxNhIEwf5h"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"jIf06WyqbCsP1is"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Unlike","logprobs":[],"obfuscation":"0UnxmoTXo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" other","logprobs":[],"obfuscation":"D082q19raq"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robots","logprobs":[],"obfuscation":"O6qMHEj2b"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" whose","logprobs":[],"obfuscation":"vee013IYPw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" tasks","logprobs":[],"obfuscation":"XHa10h45Oa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" revol","logprobs":[],"obfuscation":"6FBrIwdGV9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ved","logprobs":[],"obfuscation":"M0VL3Bw0RIAo6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" around","logprobs":[],"obfuscation":"LLilH7SVr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" heavy","logprobs":[],"obfuscation":"tegXm6RO6A"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lifting","logprobs":[],"obfuscation":"6b3EMVcS"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"JhqGeJLj5aA3V"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" data","logprobs":[],"obfuscation":"2GzCA3ZBZov"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" processing","logprobs":[],"obfuscation":"pQJMQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"yIf9YenbsIenASh"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"wKzF15AosR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"Wowp4nS4X1Ng"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" designed","logprobs":[],"obfuscation":"Yz6ZJdQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"zf1HLk47LNX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"sNucb47CLCVlI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" intricate","logprobs":[],"obfuscation":"9TxqRk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" array","logprobs":[],"obfuscation":"d2GG2LyctD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"yy31Pt217J6Xp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sensors","logprobs":[],"obfuscation":"dFE11Kjt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"j5OIdm87111a"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"WwEaIsudqLtCvf"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" flexible","logprobs":[],"obfuscation":"jH5YA59"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" arm","logprobs":[],"obfuscation":"RJVKiLoNoYxQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"bpX63CPMF8aQHv7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" perfect","logprobs":[],"obfuscation":"eCXfxPet"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"aNwYIhOgicEt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" creativity","logprobs":[],"obfuscation":"QTeqK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"qFaBkm23u4NYkj4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" However","logprobs":[],"obfuscation":"hrYOmahs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"PnGLt5WSzXM3RG4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"yk2yG2xNbY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" had","logprobs":[],"obfuscation":"CShj4jWsDFmW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" never","logprobs":[],"obfuscation":"b92hQra8IU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painted","logprobs":[],"obfuscation":"Wu9kSosu"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"41kdUr8fcF1eY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"One","logprobs":[],"obfuscation":"ywv21ub1bYPzr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rainy","logprobs":[],"obfuscation":"piDyieWe6I"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" afternoon","logprobs":[],"obfuscation":"o6TtQn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"6K5zBbkZ1KDqaOo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" while","logprobs":[],"obfuscation":"DZpPr8CLVs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" organizing","logprobs":[],"obfuscation":"yyd7A"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"TbeYUHmhLW"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"brush","logprobs":[],"obfuscation":"LSTcAO85OyQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"es","logprobs":[],"obfuscation":"g8YnY0jNlHqwv8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Ey5F23xj6FJr"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" canv","logprobs":[],"obfuscation":"EsQE9gBSUI5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ases","logprobs":[],"obfuscation":"jXPKC0ARj6Jk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"p0APw0fonPMBbpz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"S9Iw9WD1td"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" stumbled","logprobs":[],"obfuscation":"lUhKO2y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" across","logprobs":[],"obfuscation":"zOVN5cc6m"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"kFX7KcjAVQa3u"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" old","logprobs":[],"obfuscation":"PcJzaliXOTKf"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"5JFpUDK"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"—a","logprobs":[],"obfuscation":"hN488ItRbxIdlD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" dazzling","logprobs":[],"obfuscation":"JEkA0aE"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"mehmYO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bursting","logprobs":[],"obfuscation":"gq0lWWG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"roG9ZXQbDpe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"gdKUt6ALG"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"UUCXxD95v3ekSVk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Fasc","logprobs":[],"obfuscation":"iVOZvBK0g9g"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"inated","logprobs":[],"obfuscation":"WyckQbiJri"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"qPzZ3PZNvSoTVXz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"YKdVPbL14g"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" studied","logprobs":[],"obfuscation":"j6lPd2xU"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"3zYfSjrWfRlp"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"ygVKhmv"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"s","logprobs":[],"obfuscation":"jfyEtMpt46t1Ww"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sw","logprobs":[],"obfuscation":"8ufXFBggxZ3TS"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"irls","logprobs":[],"obfuscation":"SbzWkGTAG34r"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"hXqSM3Qr77XDVdb"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" textures","logprobs":[],"obfuscation":"OoYDmdA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"WYukNpLZWJs1j5L"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"O9CtJKsoG2JB"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"uoha0aPHY3w7"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" way","logprobs":[],"obfuscation":"KnlsDOXhAPma"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"Nqzf9hidx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" danced","logprobs":[],"obfuscation":"hhZcUfldt"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" together","logprobs":[],"obfuscation":"Mnd309k"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"PqZH6hxgnvJ1z1S"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" An","logprobs":[],"obfuscation":"rgthuRNYqDVfd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" idea","logprobs":[],"obfuscation":"RYoJHQzMviw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sparked","logprobs":[],"obfuscation":"bFn7eHwA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"Ym8ImtIUdMlm3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"2HuZRNAzdFY5"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" circuits","logprobs":[],"obfuscation":"b19ajJd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"dBAMCGUMUgounvx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"KDcOVnk2sl"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" would","logprobs":[],"obfuscation":"QaX2I1Dg85"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" learn","logprobs":[],"obfuscation":"2QkmV1t6Js"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"1m259XNwN7CxV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"SUGIRDOxLQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"eIMGNNPhRFbU4"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"At","logprobs":[],"obfuscation":"G8GUOB6HOwqe9H"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" first","logprobs":[],"obfuscation":"4kUZs77xIL"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"DZJLHDJJJoRMgTV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"sXRNA81QPcKuI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"QLCPvdRQ7qmn"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" cl","logprobs":[],"obfuscation":"J9qOKCfVRbrtD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"umsy","logprobs":[],"obfuscation":"MV6H5FqEJNdo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"WDWm0egBq1CmII3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"hNiFWJ96FXpg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" brushes","logprobs":[],"obfuscation":"Pf0FFkql"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" slipped","logprobs":[],"obfuscation":"kwS961wY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"XyDhbqDYRBT"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"YmOIFY8YCUqL"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" grip","logprobs":[],"obfuscation":"ABcdnw5EIpX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"SXShKYz3KjctF5L"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"VMtvX3tcPsMa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"B3jtn3jGg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sme","logprobs":[],"obfuscation":"jphfFzmwPLaF"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ared","logprobs":[],"obfuscation":"TwRJ1pgJfZXY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" into","logprobs":[],"obfuscation":"jHvjvmlmRFx"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mudd","logprobs":[],"obfuscation":"8LaKYmukTFy"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"led","logprobs":[],"obfuscation":"OEby50ZgHV8mj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" gray","logprobs":[],"obfuscation":"vQLkls6KtLN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" blobs","logprobs":[],"obfuscation":"6pJRSKWLsI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" instead","logprobs":[],"obfuscation":"qImXEbxD"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"BTsJcdzYMfYed"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" vibrant","logprobs":[],"obfuscation":"Uo6JuUrd"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" hues","logprobs":[],"obfuscation":"mCwdvWFcVLe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":148,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"3rAuIoc3iI7OrtQ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":149,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" But","logprobs":[],"obfuscation":"cRhMS7RaTArm"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":150,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"M1mKyav7ph"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":151,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" persisted","logprobs":[],"obfuscation":"eF5aUk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":152,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"yDdDhy5v9Zw35r6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":153,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Each","logprobs":[],"obfuscation":"xqte6NkdiIo"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":154,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" day","logprobs":[],"obfuscation":"rppAW4RVeF8R"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":155,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"udSWzKzTyrCWVLi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":156,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"F2NUuJOxWKpjP"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":157,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" practiced","logprobs":[],"obfuscation":"Aqqlv9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":158,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"ZUk2MhldL4AtrAe"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":159,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mixing","logprobs":[],"obfuscation":"l030hejQa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":160,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paints","logprobs":[],"obfuscation":"4xlfaIzxC"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":161,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"BtVvUiDXh3jSgxs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":162,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" experimenting","logprobs":[],"obfuscation":"di"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":163,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"MCekQrhkBKN"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":164,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" strokes","logprobs":[],"obfuscation":"rRuR8dnc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":165,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"cFjk3IoxYD4tGrw"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":166,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"DQ3Xi2a9dX9y"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":167,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" observing","logprobs":[],"obfuscation":"8t7Acj"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":168,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"KDYvCe6JsoYa"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":169,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" world","logprobs":[],"obfuscation":"0rtOhI9Ffc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":170,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"oE2kAKM9"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":171,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"RGobfdV8EooR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":172,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" eyes","logprobs":[],"obfuscation":"yzrEN6uVsyR"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":173,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"MITYFimltUsuJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":174,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" artists","logprobs":[],"obfuscation":"ndi7qdrO"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":175,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"39IBhz9cxlCBc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":176,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"Pixel","logprobs":[],"obfuscation":"SmMeGRPjx9o"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":177,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" took","logprobs":[],"obfuscation":"B7Yw3oSo8OX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":178,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" inspiration","logprobs":[],"obfuscation":"M4D6"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":179,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"lVxLLEHL7zV"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":180,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sunlight","logprobs":[],"obfuscation":"I3BmRGJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":181,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" filtering","logprobs":[],"obfuscation":"P6p35d"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":182,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"8MMH2TTk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":183,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"hmfNgkY1FJ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":184,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Lkj68PREYAHG7mZ"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":185,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"SYCf7zTCaGUi"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":186,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" depths","logprobs":[],"obfuscation":"cr9Phqnz8"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":187,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"OT3aZnPvsDcmY"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":188,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fGdrYkLZHdTI"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":189,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" ocean","logprobs":[],"obfuscation":"MvxJgRFjwz"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":190,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ox6Ar9czyzkruEM"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":191,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"MKK6YDJEzPxA"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":192,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"UWEyznWlRSj3"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":193,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rhythm","logprobs":[],"obfuscation":"8E4xhBObX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":194,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"jbQAFSh8FJWWg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":195,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"cxL7t1q6yLv"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":196,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" life","logprobs":[],"obfuscation":"CnftU4BnURk"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":197,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"XucWb0a2fGIQafX"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":198,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" It","logprobs":[],"obfuscation":"pt1xzT8tzMYRs"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":199,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" copied","logprobs":[],"obfuscation":"WrTQOEVfc"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":200,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" techniques","logprobs":[],"obfuscation":"XJJzu"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":201,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"PrOd3zA9J76"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":202,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" videos","logprobs":[],"obfuscation":"fHAS8XsLg"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":203,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Hk6mknGTtruy"}
event: response.output_text.done
data: {"type":"response.output_text.done","sequence_number":204,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and","logprobs":[]}
event: response.content_part.done
data: {"type":"response.content_part.done","sequence_number":205,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}}
event: response.output_item.done
data: {"type":"response.output_item.done","sequence_number":206,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}}
event: response.incomplete
data: {"type":"response.incomplete","sequence_number":207,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"incomplete","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the paintings swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":19,"input_tokens_details":{"cached_tokens":0},"output_tokens":200,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":219},"user":null,"metadata":{}}}
@@ -0,0 +1,17 @@
{
"metadata": {
"test_type": "create_with_initial_items"
},
"items": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What is the capital of France?"
}
]
}
]
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb980bccfc8195a9ba32b164e8a69408e61fbaa91b0a18",
"object": "conversation",
"created_at": 1761318923,
"metadata": {
"test_type": "create_with_initial_items"
}
}
@@ -0,0 +1,5 @@
{
"id": "conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8",
"object": "conversation.deleted",
"deleted": true
}
@@ -0,0 +1,5 @@
{
"id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822",
"object": "conversation.item.deleted",
"deleted": true
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Conversation with id 'conv_nonexistent123' not found.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Conversation with id 'conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8' not found.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test": "invalid"
}
// missing closing brace and has comment which is invalid JSON
@@ -0,0 +1,8 @@
{
"error": {
"message": "Invalid body: failed to parse JSON value. Please check the value to ensure it is valid JSON. (Common errors include trailing commas, missing closing brackets, missing quotation marks, etc.)",
"type": "invalid_request_error",
"param": null,
"code": "invalid_json"
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Invalid 'limit': integer above maximum value. Expected a value <= 100, but got 1000 instead.",
"type": "invalid_request_error",
"param": "limit",
"code": "integer_above_max_value"
}
}
@@ -0,0 +1,8 @@
{
"error": {
"message": "Item with id 'msg_msg_nonexistent123nonexistent123' not found in conversation.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
@@ -0,0 +1,13 @@
{
"items": [
{
"type": "message",
"content": [
{
"type": "input_text",
"text": "Hello"
}
]
}
]
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "image_input_conversation"
}
}
@@ -0,0 +1,8 @@
{
"id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7",
"object": "conversation",
"created_at": 1761319071,
"metadata": {
"test_type": "image_input_conversation"
}
}
@@ -0,0 +1,21 @@
{
"model": "gpt-4o-mini",
"conversation": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What's in this image? Describe it in detail."
},
{
"type": "input_image",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
}
]
}
],
"max_output_tokens": 200
}
@@ -0,0 +1,70 @@
{
"id": "resp_003edf96db5b4ed70068fb98bd80808194b25763125111fffa",
"object": "response",
"created_at": 1761319101,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"conversation": {
"id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": 200,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_003edf96db5b4ed70068fb98c1197481949e138bc36200ee18",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The image depicts a serene natural landscape featuring a wooden boardwalk winding through lush greenery. \n\n### Details:\n- **Pathway**: The boardwalk is made of wooden planks and extends straight ahead, encouraging exploration.\n- **Grass**: On both sides of the pathway, there is tall, vibrant green grass, suggesting a lush environment with possible wildflowers.\n- **Surrounding Vegetation**: Beyond the grass, there are various bushes and trees, adding layers of texture and color. Some foliage appears dense and lush, while other areas have more sparse coverage.\n- **Sky**: The sky is expansive and bright, with soft, fluffy clouds scattered throughout. The blue hues create a tranquil atmosphere, illuminated by sunlight.\n- **Overall Mood**: The scene conveys a sense of peace and openness, perfect for a nature walk or outdoor meditation.\n\nThis idyllic setting invites the viewer to appreciate the tranquility of nature and the beauty of the landscape."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 36852,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 192,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 37044
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,5 @@
{
"metadata": {
"test_type": "image_input_streaming"
}
}

Some files were not shown because too many files have changed in this diff Show More