diff --git a/sdk_v2/cs/src/FoundryLocalManager.cs b/sdk_v2/cs/src/FoundryLocalManager.cs
index 639be3a2..4437183f 100644
--- a/sdk_v2/cs/src/FoundryLocalManager.cs
+++ b/sdk_v2/cs/src/FoundryLocalManager.cs
@@ -150,6 +150,24 @@ await Utils.CallWithExceptionHandling(() => EnsureEpsDownloadedImplAsync(ct),
.ConfigureAwait(false);
}
+ ///
+ /// Creates an OpenAI Responses API client.
+ /// The web service must be started first via .
+ ///
+ /// Optional default model ID for requests.
+ /// An instance.
+ /// If the web service is not running.
+ public OpenAIResponsesClient GetResponsesClient(string? modelId = null)
+ {
+ if (Urls == null || Urls.Length == 0)
+ {
+ throw new FoundryLocalException(
+ "Web service is not running. Call StartWebServiceAsync before creating a ResponsesClient.", _logger);
+ }
+
+ return new OpenAIResponsesClient(Urls[0], modelId);
+ }
+
private FoundryLocalManager(Configuration configuration, ILogger logger)
{
_config = configuration ?? throw new ArgumentNullException(nameof(configuration));
diff --git a/sdk_v2/cs/src/IModel.cs b/sdk_v2/cs/src/IModel.cs
index c3acba61..772daeac 100644
--- a/sdk_v2/cs/src/IModel.cs
+++ b/sdk_v2/cs/src/IModel.cs
@@ -67,4 +67,13 @@ Task DownloadAsync(Action? downloadProgress = null,
/// Optional cancellation token.
/// OpenAI.AudioClient
Task GetAudioClientAsync(CancellationToken? ct = null);
+
+ ///
+ /// Get an OpenAI Responses API client.
+ /// Unlike Chat/Audio clients (which use FFI), the Responses API is HTTP-based,
+ /// so the web service must be started first via .
+ ///
+ /// Optional cancellation token.
+ /// OpenAI.ResponsesClient
+ Task GetResponsesClientAsync(CancellationToken? ct = null);
}
diff --git a/sdk_v2/cs/src/Model.cs b/sdk_v2/cs/src/Model.cs
index bbbbcb5b..dab396ba 100644
--- a/sdk_v2/cs/src/Model.cs
+++ b/sdk_v2/cs/src/Model.cs
@@ -113,6 +113,11 @@ public async Task GetAudioClientAsync(CancellationToken? ct =
return await SelectedVariant.GetAudioClientAsync(ct).ConfigureAwait(false);
}
+ public async Task GetResponsesClientAsync(CancellationToken? ct = null)
+ {
+ return await SelectedVariant.GetResponsesClientAsync(ct).ConfigureAwait(false);
+ }
+
public async Task UnloadAsync(CancellationToken? ct = null)
{
await SelectedVariant.UnloadAsync(ct).ConfigureAwait(false);
diff --git a/sdk_v2/cs/src/ModelVariant.cs b/sdk_v2/cs/src/ModelVariant.cs
index 6ca7cda7..5593b18f 100644
--- a/sdk_v2/cs/src/ModelVariant.cs
+++ b/sdk_v2/cs/src/ModelVariant.cs
@@ -100,6 +100,13 @@ public async Task GetAudioClientAsync(CancellationToken? ct =
.ConfigureAwait(false);
}
+ public async Task GetResponsesClientAsync(CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(() => GetResponsesClientImplAsync(ct),
+ "Error getting responses client for model", _logger)
+ .ConfigureAwait(false);
+ }
+
private async Task IsLoadedImplAsync(CancellationToken? ct = null)
{
var loadedModels = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false);
@@ -190,4 +197,21 @@ private async Task GetAudioClientImplAsync(CancellationToken?
return new OpenAIAudioClient(Id);
}
+
+ private async Task GetResponsesClientImplAsync(CancellationToken? ct = null)
+ {
+ if (!await IsLoadedAsync(ct))
+ {
+ throw new FoundryLocalException($"Model {Id} is not loaded. Call LoadAsync first.");
+ }
+
+ var urls = FoundryLocalManager.Instance.Urls;
+ if (urls == null || urls.Length == 0)
+ {
+ throw new FoundryLocalException(
+ "Web service is not running. Call StartWebServiceAsync before creating a ResponsesClient.");
+ }
+
+ return new OpenAIResponsesClient(urls[0], Id);
+ }
}
diff --git a/sdk_v2/cs/src/OpenAI/ResponsesClient.cs b/sdk_v2/cs/src/OpenAI/ResponsesClient.cs
new file mode 100644
index 00000000..bf7240ac
--- /dev/null
+++ b/sdk_v2/cs/src/OpenAI/ResponsesClient.cs
@@ -0,0 +1,455 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Microsoft.AI.Foundry.Local;
+
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Channels;
+
+using Microsoft.AI.Foundry.Local.OpenAI;
+using Microsoft.Extensions.Logging;
+
+///
+/// Client for the OpenAI Responses API served by Foundry Local's embedded web service.
+///
+/// Unlike and (which use FFI via CoreInterop),
+/// the Responses API is HTTP-only. This client uses HttpClient for all operations and parses
+/// Server-Sent Events for streaming.
+///
+/// Create via or
+/// .
+///
+public class OpenAIResponsesClient : IDisposable
+{
+ private readonly string _baseUrl;
+ private readonly string? _modelId;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger = FoundryLocalManager.Instance.Logger;
+ private bool _disposed;
+
+ ///
+ /// Settings for the Responses API client.
+ ///
+ public ResponsesSettings Settings { get; } = new();
+
+ internal OpenAIResponsesClient(string baseUrl, string? modelId)
+ {
+ if (string.IsNullOrWhiteSpace(baseUrl))
+ {
+ throw new ArgumentException("baseUrl must be a non-empty string.", nameof(baseUrl));
+ }
+
+ _baseUrl = baseUrl.TrimEnd('/');
+ _modelId = modelId;
+#pragma warning disable IDISP014 // Use a single instance of HttpClient — lifetime is tied to this client
+ _httpClient = new HttpClient();
+#pragma warning restore IDISP014
+ }
+
+ ///
+ /// Creates a model response (non-streaming).
+ ///
+ /// A string prompt or structured input.
+ /// Optional cancellation token.
+ /// The completed Response object. Check Status and Error even on HTTP 200.
+ public async Task CreateAsync(string input, CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => CreateImplAsync(new ResponseInput { Text = input }, options: null, ct),
+ "Error creating response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Creates a model response (non-streaming) with additional options.
+ ///
+ public async Task CreateAsync(string input, Action? options,
+ CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => CreateImplAsync(new ResponseInput { Text = input }, options, ct),
+ "Error creating response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Creates a model response (non-streaming) from structured input items.
+ ///
+ public async Task CreateAsync(List input, CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => CreateImplAsync(new ResponseInput { Items = input }, options: null, ct),
+ "Error creating response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Creates a model response (non-streaming) from structured input items with options.
+ ///
+ public async Task CreateAsync(List input, Action? options,
+ CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => CreateImplAsync(new ResponseInput { Items = input }, options, ct),
+ "Error creating response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Creates a streaming response, returning events as an async enumerable.
+ ///
+ /// A string prompt.
+ /// Cancellation token.
+ /// Async enumerable of streaming events.
+ public IAsyncEnumerable CreateStreamingAsync(string input,
+ CancellationToken ct)
+ {
+ return CreateStreamingAsync(input, options: null, ct);
+ }
+
+ ///
+ /// Creates a streaming response with options.
+ ///
+ public async IAsyncEnumerable CreateStreamingAsync(string input,
+ Action? options,
+ [EnumeratorCancellation] CancellationToken ct)
+ {
+ var enumerable = Utils.CallWithExceptionHandling(
+ () => StreamingImplAsync(new ResponseInput { Text = input }, options, ct),
+ "Error during streaming response.", _logger).ConfigureAwait(false);
+
+ await foreach (var item in enumerable)
+ {
+ yield return item;
+ }
+ }
+
+ ///
+ /// Creates a streaming response from structured input items.
+ ///
+ public IAsyncEnumerable CreateStreamingAsync(List input,
+ CancellationToken ct)
+ {
+ return CreateStreamingAsync(input, options: null, ct);
+ }
+
+ ///
+ /// Creates a streaming response from structured input items with options.
+ ///
+ public async IAsyncEnumerable CreateStreamingAsync(List input,
+ Action? options,
+ [EnumeratorCancellation] CancellationToken ct)
+ {
+ var enumerable = Utils.CallWithExceptionHandling(
+ () => StreamingImplAsync(new ResponseInput { Items = input }, options, ct),
+ "Error during streaming response.", _logger).ConfigureAwait(false);
+
+ await foreach (var item in enumerable)
+ {
+ yield return item;
+ }
+ }
+
+ ///
+ /// Retrieves a stored response by ID.
+ ///
+ public async Task GetAsync(string responseId, CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => GetImplAsync(responseId, ct),
+ "Error retrieving response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Deletes a stored response by ID.
+ ///
+ public async Task DeleteAsync(string responseId, CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => DeleteImplAsync(responseId, ct),
+ "Error deleting response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Cancels an in-progress response.
+ ///
+ public async Task CancelAsync(string responseId, CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => CancelImplAsync(responseId, ct),
+ "Error cancelling response.", _logger).ConfigureAwait(false);
+ }
+
+ ///
+ /// Retrieves the input items for a stored response.
+ ///
+ public async Task GetInputItemsAsync(string responseId,
+ CancellationToken? ct = null)
+ {
+ return await Utils.CallWithExceptionHandling(
+ () => GetInputItemsImplAsync(responseId, ct),
+ "Error retrieving input items.", _logger).ConfigureAwait(false);
+ }
+
+ // ========================================================================
+ // Implementation methods
+ // ========================================================================
+
+ private async Task CreateImplAsync(ResponseInput input,
+ Action? options,
+ CancellationToken? ct)
+ {
+ var request = BuildRequest(input, stream: false);
+ options?.Invoke(request);
+
+ var json = JsonSerializer.Serialize(request, ResponsesJsonContext.Default.ResponseCreateRequest);
+ using var content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ using var response = await _httpClient.PostAsync($"{_baseUrl}/v1/responses", content,
+ ct ?? CancellationToken.None).ConfigureAwait(false);
+ await EnsureSuccessAsync(response, ct ?? CancellationToken.None).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(body, ResponsesJsonContext.Default.ResponseObject)
+ ?? throw new FoundryLocalException($"Failed to deserialize response: {body[..Math.Min(body.Length, 200)]}", _logger);
+ }
+
+ private async IAsyncEnumerable StreamingImplAsync(
+ ResponseInput input,
+ Action? options,
+ [EnumeratorCancellation] CancellationToken ct)
+ {
+ var request = BuildRequest(input, stream: true);
+ options?.Invoke(request);
+
+ var json = JsonSerializer.Serialize(request, ResponsesJsonContext.Default.ResponseCreateRequest);
+ using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/v1/responses")
+ {
+ Content = new StringContent(json, Encoding.UTF8, "application/json")
+ };
+ httpRequest.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream"));
+
+ // Cannot use 'using' on response in an async iterator — the response stream's lifetime
+ // is tied to the IAsyncEnumerable consumer. The StreamReader disposal will close the stream.
+#pragma warning disable IDISP001 // Dispose created
+ var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, ct)
+ .ConfigureAwait(false);
+#pragma warning restore IDISP001
+ await EnsureSuccessAsync(response, ct).ConfigureAwait(false);
+
+ var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
+ using var reader = new StreamReader(stream);
+
+ var dataLines = new List();
+
+ while (!reader.EndOfStream && !ct.IsCancellationRequested)
+ {
+ var line = await reader.ReadLineAsync(ct).ConfigureAwait(false);
+
+ if (line == null)
+ {
+ break;
+ }
+
+ // Empty line = end of SSE block
+ if (line.Length == 0)
+ {
+ if (dataLines.Count > 0)
+ {
+ var eventData = string.Join("\n", dataLines);
+ dataLines.Clear();
+
+ // Terminal signal
+ if (eventData == "[DONE]")
+ {
+ yield break;
+ }
+
+ ResponseStreamingEvent? evt;
+ try
+ {
+ evt = JsonSerializer.Deserialize(eventData, ResponsesJsonContext.Default.ResponseStreamingEvent);
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Failed to parse SSE event: {Data}", eventData);
+ continue;
+ }
+
+ if (evt != null)
+ {
+ yield return evt;
+ }
+ }
+
+ continue;
+ }
+
+ // Collect data lines
+ if (line.StartsWith("data: ", StringComparison.Ordinal))
+ {
+ dataLines.Add(line[6..]);
+ }
+ else if (line == "data:")
+ {
+ dataLines.Add(string.Empty);
+ }
+ // 'event:' lines are informational; type is inside the JSON
+ }
+ }
+
+ private async Task GetImplAsync(string responseId, CancellationToken? ct)
+ {
+ ValidateId(responseId, nameof(responseId));
+ using var response = await _httpClient.GetAsync(
+ $"{_baseUrl}/v1/responses/{Uri.EscapeDataString(responseId)}",
+ ct ?? CancellationToken.None).ConfigureAwait(false);
+ await EnsureSuccessAsync(response, ct ?? CancellationToken.None).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(body, ResponsesJsonContext.Default.ResponseObject)
+ ?? throw new FoundryLocalException($"Failed to deserialize response: {body[..Math.Min(body.Length, 200)]}", _logger);
+ }
+
+ private async Task DeleteImplAsync(string responseId, CancellationToken? ct)
+ {
+ ValidateId(responseId, nameof(responseId));
+ using var response = await _httpClient.DeleteAsync(
+ $"{_baseUrl}/v1/responses/{Uri.EscapeDataString(responseId)}",
+ ct ?? CancellationToken.None).ConfigureAwait(false);
+ await EnsureSuccessAsync(response, ct ?? CancellationToken.None).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(body, ResponsesJsonContext.Default.ResponseDeleteResult)
+ ?? throw new FoundryLocalException($"Failed to deserialize delete result: {body[..Math.Min(body.Length, 200)]}", _logger);
+ }
+
+ private async Task CancelImplAsync(string responseId, CancellationToken? ct)
+ {
+ ValidateId(responseId, nameof(responseId));
+ using var cancelResponse = await _httpClient.PostAsync(
+ $"{_baseUrl}/v1/responses/{Uri.EscapeDataString(responseId)}/cancel",
+ null, ct ?? CancellationToken.None).ConfigureAwait(false);
+ await EnsureSuccessAsync(cancelResponse, ct ?? CancellationToken.None).ConfigureAwait(false);
+
+ var body = await cancelResponse.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(body, ResponsesJsonContext.Default.ResponseObject)
+ ?? throw new FoundryLocalException($"Failed to deserialize response: {body[..Math.Min(body.Length, 200)]}", _logger);
+ }
+
+ private async Task GetInputItemsImplAsync(string responseId,
+ CancellationToken? ct)
+ {
+ ValidateId(responseId, nameof(responseId));
+ using var response = await _httpClient.GetAsync(
+ $"{_baseUrl}/v1/responses/{Uri.EscapeDataString(responseId)}/input_items",
+ ct ?? CancellationToken.None).ConfigureAwait(false);
+ await EnsureSuccessAsync(response, ct ?? CancellationToken.None).ConfigureAwait(false);
+
+ var body = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(body, ResponsesJsonContext.Default.ResponseInputItemsList)
+ ?? throw new FoundryLocalException($"Failed to deserialize input items: {body[..Math.Min(body.Length, 200)]}", _logger);
+ }
+
+ // ========================================================================
+ // Helpers
+ // ========================================================================
+
+ private ResponseCreateRequest BuildRequest(ResponseInput input, bool stream)
+ {
+ var model = _modelId;
+ if (string.IsNullOrWhiteSpace(model))
+ {
+ throw new FoundryLocalException(
+ "Model must be specified either in the constructor or via GetResponsesClientAsync(modelId).");
+ }
+
+ // Merge order: model+input → settings defaults → per-call overrides (via Action)
+ return new ResponseCreateRequest
+ {
+ Model = model,
+ Input = input,
+ Stream = stream,
+ Instructions = Settings.Instructions,
+ Temperature = Settings.Temperature,
+ TopP = Settings.TopP,
+ MaxOutputTokens = Settings.MaxOutputTokens,
+ FrequencyPenalty = Settings.FrequencyPenalty,
+ PresencePenalty = Settings.PresencePenalty,
+ ToolChoice = Settings.ToolChoice,
+ Truncation = Settings.Truncation,
+ ParallelToolCalls = Settings.ParallelToolCalls,
+ Store = Settings.Store,
+ Metadata = Settings.Metadata,
+ Reasoning = Settings.Reasoning,
+ Text = Settings.Text,
+ Seed = Settings.Seed,
+ };
+ }
+
+ private static void ValidateId(string id, string paramName)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentException($"{paramName} must be a non-empty string.", paramName);
+ }
+
+ if (id.Length > 1024)
+ {
+ throw new ArgumentException($"{paramName} exceeds maximum length (1024).", paramName);
+ }
+ }
+
+ private async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken ct = default)
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorBody = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
+ throw new FoundryLocalException(
+ $"Responses API error ({(int)response.StatusCode}): {errorBody}", _logger);
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _httpClient.Dispose();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
+
+///
+/// Settings for the Responses API.
+///
+public record ResponsesSettings
+{
+ /// System-level instructions to guide the model.
+ public string? Instructions { get; set; }
+ public float? Temperature { get; set; }
+ public float? TopP { get; set; }
+ public int? MaxOutputTokens { get; set; }
+ public float? FrequencyPenalty { get; set; }
+ public float? PresencePenalty { get; set; }
+ public ResponseToolChoice? ToolChoice { get; set; }
+ public string? Truncation { get; set; }
+ public bool? ParallelToolCalls { get; set; }
+ public bool? Store { get; set; }
+ public Dictionary? Metadata { get; set; }
+ public ResponseReasoningConfig? Reasoning { get; set; }
+ public ResponseTextConfig? Text { get; set; }
+ public int? Seed { get; set; }
+}
diff --git a/sdk_v2/cs/src/OpenAI/ResponsesJsonContext.cs b/sdk_v2/cs/src/OpenAI/ResponsesJsonContext.cs
new file mode 100644
index 00000000..bc890ef8
--- /dev/null
+++ b/sdk_v2/cs/src/OpenAI/ResponsesJsonContext.cs
@@ -0,0 +1,44 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Microsoft.AI.Foundry.Local.OpenAI;
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+[JsonSerializable(typeof(ResponseCreateRequest))]
+[JsonSerializable(typeof(ResponseObject))]
+[JsonSerializable(typeof(ResponseDeleteResult))]
+[JsonSerializable(typeof(ResponseInputItemsList))]
+[JsonSerializable(typeof(ResponseStreamingEvent))]
+[JsonSerializable(typeof(ResponseItem))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(ResponseMessageItem))]
+[JsonSerializable(typeof(ResponseFunctionCallItem))]
+[JsonSerializable(typeof(ResponseFunctionCallOutputItem))]
+[JsonSerializable(typeof(ResponseContentPart))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(ResponseInputTextContent))]
+[JsonSerializable(typeof(ResponseOutputTextContent))]
+[JsonSerializable(typeof(ResponseRefusalContent))]
+[JsonSerializable(typeof(ResponseFunctionTool))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(ResponseSpecificToolChoice))]
+[JsonSerializable(typeof(ResponseUsage))]
+[JsonSerializable(typeof(ResponseError))]
+[JsonSerializable(typeof(ResponseIncompleteDetails))]
+[JsonSerializable(typeof(ResponseReasoningConfig))]
+[JsonSerializable(typeof(ResponseTextConfig))]
+[JsonSerializable(typeof(ResponseTextFormat))]
+[JsonSerializable(typeof(ResponseMessageContent))]
+[JsonSerializable(typeof(ResponseInput))]
+[JsonSerializable(typeof(ResponseToolChoice))]
+[JsonSerializable(typeof(JsonElement))]
+[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = false)]
+internal partial class ResponsesJsonContext : JsonSerializerContext
+{
+}
diff --git a/sdk_v2/cs/src/OpenAI/ResponsesTypes.cs b/sdk_v2/cs/src/OpenAI/ResponsesTypes.cs
new file mode 100644
index 00000000..937cff2a
--- /dev/null
+++ b/sdk_v2/cs/src/OpenAI/ResponsesTypes.cs
@@ -0,0 +1,628 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Microsoft.AI.Foundry.Local.OpenAI;
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+// ============================================================================
+// Responses API Types
+// Aligned with OpenAI Responses API / OpenResponses spec.
+// ============================================================================
+
+#region Request Types
+
+///
+/// Request body for POST /v1/responses.
+///
+public class ResponseCreateRequest
+{
+ [JsonPropertyName("model")]
+ public required string Model { get; set; }
+
+ [JsonPropertyName("input")]
+ public ResponseInput? Input { get; set; }
+
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; set; }
+
+ [JsonPropertyName("previous_response_id")]
+ public string? PreviousResponseId { get; set; }
+
+ [JsonPropertyName("tools")]
+ public List? Tools { get; set; }
+
+ [JsonPropertyName("tool_choice")]
+ [JsonConverter(typeof(ResponseToolChoiceConverter))]
+ public ResponseToolChoice? ToolChoice { get; set; }
+
+ [JsonPropertyName("temperature")]
+ public float? Temperature { get; set; }
+
+ [JsonPropertyName("top_p")]
+ public float? TopP { get; set; }
+
+ [JsonPropertyName("max_output_tokens")]
+ public int? MaxOutputTokens { get; set; }
+
+ [JsonPropertyName("frequency_penalty")]
+ public float? FrequencyPenalty { get; set; }
+
+ [JsonPropertyName("presence_penalty")]
+ public float? PresencePenalty { get; set; }
+
+ [JsonPropertyName("truncation")]
+ public string? Truncation { get; set; }
+
+ [JsonPropertyName("parallel_tool_calls")]
+ public bool? ParallelToolCalls { get; set; }
+
+ [JsonPropertyName("store")]
+ public bool? Store { get; set; }
+
+ [JsonPropertyName("metadata")]
+ public Dictionary? Metadata { get; set; }
+
+ [JsonPropertyName("stream")]
+ public bool? Stream { get; set; }
+
+ [JsonPropertyName("reasoning")]
+ public ResponseReasoningConfig? Reasoning { get; set; }
+
+ [JsonPropertyName("text")]
+ public ResponseTextConfig? Text { get; set; }
+
+ [JsonPropertyName("seed")]
+ public int? Seed { get; set; }
+
+ [JsonPropertyName("user")]
+ public string? User { get; set; }
+}
+
+///
+/// Union type for input: either a plain string or an array of items.
+///
+[JsonConverter(typeof(ResponseInputConverter))]
+public class ResponseInput
+{
+ public string? Text { get; set; }
+ public List? Items { get; set; }
+
+ public static implicit operator ResponseInput(string text) => new() { Text = text };
+ public static implicit operator ResponseInput(List items) => new() { Items = items };
+}
+
+#endregion
+
+#region Response Object
+
+///
+/// The Response object returned by the Responses API.
+///
+public class ResponseObject
+{
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("object")]
+ public string ObjectType { get; set; } = "response";
+
+ [JsonPropertyName("created_at")]
+ public long CreatedAt { get; set; }
+
+ [JsonPropertyName("completed_at")]
+ public long? CompletedAt { get; set; }
+
+ [JsonPropertyName("failed_at")]
+ public long? FailedAt { get; set; }
+
+ [JsonPropertyName("cancelled_at")]
+ public long? CancelledAt { get; set; }
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; } = string.Empty;
+
+ [JsonPropertyName("incomplete_details")]
+ public ResponseIncompleteDetails? IncompleteDetails { get; set; }
+
+ [JsonPropertyName("model")]
+ public string Model { get; set; } = string.Empty;
+
+ [JsonPropertyName("previous_response_id")]
+ public string? PreviousResponseId { get; set; }
+
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; set; }
+
+ [JsonPropertyName("output")]
+ public List Output { get; set; } = [];
+
+ [JsonPropertyName("error")]
+ public ResponseError? Error { get; set; }
+
+ [JsonPropertyName("tools")]
+ public List Tools { get; set; } = [];
+
+ [JsonPropertyName("tool_choice")]
+ [JsonConverter(typeof(ResponseToolChoiceConverter))]
+ public ResponseToolChoice? ToolChoice { get; set; }
+
+ [JsonPropertyName("truncation")]
+ public string? Truncation { get; set; }
+
+ [JsonPropertyName("parallel_tool_calls")]
+ public bool ParallelToolCalls { get; set; }
+
+ [JsonPropertyName("text")]
+ public ResponseTextConfig? Text { get; set; }
+
+ [JsonPropertyName("top_p")]
+ public float TopP { get; set; }
+
+ [JsonPropertyName("temperature")]
+ public float Temperature { get; set; }
+
+ [JsonPropertyName("presence_penalty")]
+ public float PresencePenalty { get; set; }
+
+ [JsonPropertyName("frequency_penalty")]
+ public float FrequencyPenalty { get; set; }
+
+ [JsonPropertyName("max_output_tokens")]
+ public int? MaxOutputTokens { get; set; }
+
+ [JsonPropertyName("reasoning")]
+ public ResponseReasoningConfig? Reasoning { get; set; }
+
+ [JsonPropertyName("store")]
+ public bool Store { get; set; }
+
+ [JsonPropertyName("metadata")]
+ public Dictionary? Metadata { get; set; }
+
+ [JsonPropertyName("usage")]
+ public ResponseUsage? Usage { get; set; }
+
+ [JsonPropertyName("user")]
+ public string? User { get; set; }
+
+ ///
+ /// Extracts the text from the first assistant message in the output.
+ /// Equivalent to OpenAI Python SDK's response.output_text.
+ ///
+ [JsonIgnore]
+ public string OutputText
+ {
+ get
+ {
+ foreach (var item in Output)
+ {
+ if (item is ResponseMessageItem msg && msg.Role == "assistant")
+ {
+ return msg.GetText();
+ }
+ }
+ return string.Empty;
+ }
+ }
+}
+
+#endregion
+
+#region Items (input & output)
+
+///
+/// Base class for all response items using polymorphic serialization.
+///
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
+[JsonDerivedType(typeof(ResponseMessageItem), "message")]
+[JsonDerivedType(typeof(ResponseFunctionCallItem), "function_call")]
+[JsonDerivedType(typeof(ResponseFunctionCallOutputItem), "function_call_output")]
+public class ResponseItem
+{
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("status")]
+ public string? Status { get; set; }
+}
+
+public sealed class ResponseMessageItem : ResponseItem
+{
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = string.Empty;
+
+ [JsonPropertyName("content")]
+ [JsonConverter(typeof(MessageContentConverter))]
+ public ResponseMessageContent Content { get; set; } = new();
+
+ public string GetText()
+ {
+ if (Content.Text != null)
+ {
+ return Content.Text;
+ }
+
+ if (Content.Parts != null)
+ {
+ return string.Concat(Content.Parts
+ .Where(p => p is ResponseOutputTextContent)
+ .Cast()
+ .Select(p => p.Text));
+ }
+
+ return string.Empty;
+ }
+}
+
+public sealed class ResponseFunctionCallItem : ResponseItem
+{
+ [JsonPropertyName("call_id")]
+ public string CallId { get; set; } = string.Empty;
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("arguments")]
+ public string Arguments { get; set; } = string.Empty;
+}
+
+public sealed class ResponseFunctionCallOutputItem : ResponseItem
+{
+ [JsonPropertyName("call_id")]
+ public string CallId { get; set; } = string.Empty;
+
+ [JsonPropertyName("output")]
+ public string Output { get; set; } = string.Empty;
+}
+
+#endregion
+
+#region Content Parts
+
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
+[JsonDerivedType(typeof(ResponseInputTextContent), "input_text")]
+[JsonDerivedType(typeof(ResponseOutputTextContent), "output_text")]
+[JsonDerivedType(typeof(ResponseRefusalContent), "refusal")]
+public class ResponseContentPart
+{
+}
+
+public sealed class ResponseInputTextContent : ResponseContentPart
+{
+ [JsonPropertyName("text")]
+ public string Text { get; set; } = string.Empty;
+}
+
+public sealed class ResponseOutputTextContent : ResponseContentPart
+{
+ [JsonPropertyName("text")]
+ public string Text { get; set; } = string.Empty;
+
+ [JsonPropertyName("annotations")]
+ public List? Annotations { get; set; }
+}
+
+public sealed class ResponseRefusalContent : ResponseContentPart
+{
+ [JsonPropertyName("refusal")]
+ public string Refusal { get; set; } = string.Empty;
+}
+
+///
+/// Union type for message content: either a plain string or an array of content parts.
+///
+[JsonConverter(typeof(MessageContentConverter))]
+public class ResponseMessageContent
+{
+ public string? Text { get; set; }
+ public List? Parts { get; set; }
+}
+
+#endregion
+
+#region Tool Types
+
+public class ResponseFunctionTool
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "function";
+
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ [JsonPropertyName("parameters")]
+ public JsonElement? Parameters { get; set; }
+
+ [JsonPropertyName("strict")]
+ public bool? Strict { get; set; }
+}
+
+///
+/// Tool choice: either a string value ("none", "auto", "required") or a specific function reference.
+///
+public class ResponseToolChoice
+{
+ public string? Value { get; set; }
+ public ResponseSpecificToolChoice? Specific { get; set; }
+
+ public static implicit operator ResponseToolChoice(string value) => new() { Value = value };
+
+ public static readonly ResponseToolChoice Auto = new() { Value = "auto" };
+ public static readonly ResponseToolChoice None = new() { Value = "none" };
+ public static readonly ResponseToolChoice Required = new() { Value = "required" };
+}
+
+public class ResponseSpecificToolChoice
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "function";
+
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+}
+
+#endregion
+
+#region Supporting Types
+
+public class ResponseUsage
+{
+ [JsonPropertyName("input_tokens")]
+ public int InputTokens { get; set; }
+
+ [JsonPropertyName("output_tokens")]
+ public int OutputTokens { get; set; }
+
+ [JsonPropertyName("total_tokens")]
+ public int TotalTokens { get; set; }
+}
+
+public class ResponseError
+{
+ [JsonPropertyName("code")]
+ public string Code { get; set; } = string.Empty;
+
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+}
+
+public class ResponseIncompleteDetails
+{
+ [JsonPropertyName("reason")]
+ public string Reason { get; set; } = string.Empty;
+}
+
+public class ResponseReasoningConfig
+{
+ [JsonPropertyName("effort")]
+ public string? Effort { get; set; }
+
+ [JsonPropertyName("summary")]
+ public string? Summary { get; set; }
+}
+
+public class ResponseTextConfig
+{
+ [JsonPropertyName("format")]
+ public ResponseTextFormat? Format { get; set; }
+
+ [JsonPropertyName("verbosity")]
+ public string? Verbosity { get; set; }
+}
+
+public class ResponseTextFormat
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "text";
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("schema")]
+ public JsonElement? Schema { get; set; }
+
+ [JsonPropertyName("strict")]
+ public bool? Strict { get; set; }
+}
+
+public class ResponseDeleteResult
+{
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("object")]
+ public string ObjectType { get; set; } = string.Empty;
+
+ [JsonPropertyName("deleted")]
+ public bool Deleted { get; set; }
+}
+
+public class ResponseInputItemsList
+{
+ [JsonPropertyName("object")]
+ public string ObjectType { get; set; } = "list";
+
+ [JsonPropertyName("data")]
+ public List Data { get; set; } = [];
+}
+
+#endregion
+
+#region Streaming Events
+
+///
+/// A streaming event from the Responses API SSE stream.
+///
+public class ResponseStreamingEvent
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+
+ [JsonPropertyName("sequence_number")]
+ public int SequenceNumber { get; set; }
+
+ // Lifecycle events carry the full response
+ [JsonPropertyName("response")]
+ public ResponseObject? Response { get; set; }
+
+ // Item events
+ [JsonPropertyName("item_id")]
+ public string? ItemId { get; set; }
+
+ [JsonPropertyName("output_index")]
+ public int? OutputIndex { get; set; }
+
+ [JsonPropertyName("content_index")]
+ public int? ContentIndex { get; set; }
+
+ [JsonPropertyName("item")]
+ public ResponseItem? Item { get; set; }
+
+ [JsonPropertyName("part")]
+ public ResponseContentPart? Part { get; set; }
+
+ // Text delta/done
+ [JsonPropertyName("delta")]
+ public string? Delta { get; set; }
+
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
+ // Function call args
+ [JsonPropertyName("arguments")]
+ public string? Arguments { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ // Refusal
+ [JsonPropertyName("refusal")]
+ public string? Refusal { get; set; }
+
+ // Error
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+}
+
+#endregion
+
+#region JSON Converters
+
+internal class ResponseInputConverter : JsonConverter
+{
+ public override ResponseInput? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new ResponseInput { Text = reader.GetString() };
+ }
+
+ if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ var items = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListResponseItem);
+ return new ResponseInput { Items = items };
+ }
+
+ return null;
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResponseInput value, JsonSerializerOptions options)
+ {
+ if (value.Text != null)
+ {
+ writer.WriteStringValue(value.Text);
+ }
+ else if (value.Items != null)
+ {
+ JsonSerializer.Serialize(writer, value.Items, ResponsesJsonContext.Default.ListResponseItem);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+}
+
+internal class ResponseToolChoiceConverter : JsonConverter
+{
+ public override ResponseToolChoice? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new ResponseToolChoice { Value = reader.GetString() };
+ }
+
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ var specific = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ResponseSpecificToolChoice);
+ return new ResponseToolChoice { Specific = specific };
+ }
+
+ return null;
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResponseToolChoice value, JsonSerializerOptions options)
+ {
+ if (value.Value != null)
+ {
+ writer.WriteStringValue(value.Value);
+ }
+ else if (value.Specific != null)
+ {
+ JsonSerializer.Serialize(writer, value.Specific, ResponsesJsonContext.Default.ResponseSpecificToolChoice);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+}
+
+internal class MessageContentConverter : JsonConverter
+{
+ public override ResponseMessageContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new ResponseMessageContent { Text = reader.GetString() };
+ }
+
+ if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ var parts = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListResponseContentPart);
+ return new ResponseMessageContent { Parts = parts };
+ }
+
+ return null;
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResponseMessageContent value, JsonSerializerOptions options)
+ {
+ if (value.Text != null)
+ {
+ writer.WriteStringValue(value.Text);
+ }
+ else if (value.Parts != null)
+ {
+ JsonSerializer.Serialize(writer, value.Parts, ResponsesJsonContext.Default.ListResponseContentPart);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+}
+
+#endregion