Skip to content

Commit 64e78f2

Browse files
MaanavDCopilot
andcommitted
Add OpenAI Responses API client to C# SDK
Implements `OpenAIResponsesClient` for the OpenAI Responses API surface in the C# SDK, mirroring the Python (#670) and JS (#671) SDKs and incorporating their resolved review feedback up front. - New `Microsoft.AI.Foundry.Local.OpenAI.OpenAIResponsesClient` (HTTP-only, `HttpClient`-based, no FFI) - Non-streaming + IAsyncEnumerable streaming (`Channel<T>` SSE pipeline) - Full CRUD: `GetAsync`, `DeleteAsync`, `CancelAsync`, `GetInputItemsAsync`, `ListAsync(limit, order, after)` - Polymorphic content parts and response items via `[JsonPolymorphic]` + source-gen context - Streaming events for lifecycle, output, text deltas, function calls, and reasoning - Vision helpers: `InputImageContent.FromFile/FromUrl/FromBytes` - `FoundryLocalManager.GetResponsesClient(modelId?)` and `IModel.GetResponsesClientAsync` Pre-applied PR review feedback from the Python and JS PRs: - `Settings.Store` defaults to `null` (omit) instead of forcing `store=true` - `InputImageContent.MediaType` is optional; unknown extensions omit the field so the server infers - `InputImageContent.FromFile` throws `FileNotFoundException` on missing path - `HttpClient.Timeout = Timeout.InfiniteTimeSpan`; callers use `CancellationToken` for deadlines (avoids 100s default cutting off SSE) - `ListAsync` accepts `limit`, `order`, `after`; `ListResponsesResult` exposes `first_id`, `last_id`, `has_more` - `InputImageContent.Validate()` enforces mutual exclusivity of `ImageUrl` / `ImageData` at request build time - BMP supported in `DetectMediaType` alongside png/jpg/jpeg/gif/webp - Uses shared `FoundryLocalException` for transport/parse errors, no dedicated `ResponsesException` Tests: 22 unit tests (mocked HTTP) + integration tests gated on a running service. `dotnet build sdk/cs/src` clean; all Responses tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 088f844 commit 64e78f2

10 files changed

Lines changed: 2674 additions & 1 deletion

File tree

sdk/cs/src/Detail/Model.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ public async Task<OpenAIEmbeddingClient> GetEmbeddingClientAsync(CancellationTok
104104
return await SelectedVariant.GetEmbeddingClientAsync(ct).ConfigureAwait(false);
105105
}
106106

107+
public async Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null)
108+
{
109+
return await SelectedVariant.GetResponsesClientAsync(ct).ConfigureAwait(false);
110+
}
111+
107112
public async Task UnloadAsync(CancellationToken? ct = null)
108113
{
109114
await SelectedVariant.UnloadAsync(ct).ConfigureAwait(false);

sdk/cs/src/Detail/ModelVariant.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ public async Task<OpenAIEmbeddingClient> GetEmbeddingClientAsync(CancellationTok
109109
.ConfigureAwait(false);
110110
}
111111

112+
public async Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null)
113+
{
114+
return await Utils.CallWithExceptionHandling(() => GetResponsesClientImplAsync(ct),
115+
"Error getting responses client for model", _logger)
116+
.ConfigureAwait(false);
117+
}
118+
112119
private async Task<bool> IsLoadedImplAsync(CancellationToken? ct = null)
113120
{
114121
var loadedModels = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false);
@@ -210,6 +217,27 @@ private async Task<OpenAIEmbeddingClient> GetEmbeddingClientImplAsync(Cancellati
210217
return new OpenAIEmbeddingClient(Id);
211218
}
212219

220+
private async Task<OpenAIResponsesClient> GetResponsesClientImplAsync(CancellationToken? ct = null)
221+
{
222+
if (!await IsLoadedAsync(ct))
223+
{
224+
throw new FoundryLocalException($"Model {Id} is not loaded. Call LoadAsync first.");
225+
}
226+
227+
var manager = FoundryLocalManager.Instance;
228+
if (manager.Urls == null || manager.Urls.Length == 0)
229+
{
230+
await manager.StartWebServiceAsync(ct).ConfigureAwait(false);
231+
}
232+
233+
if (manager.Urls == null || manager.Urls.Length == 0)
234+
{
235+
throw new FoundryLocalException("Web service is not running. Call StartWebServiceAsync first.");
236+
}
237+
238+
return new OpenAIResponsesClient(manager.Urls[0], Id);
239+
}
240+
213241
public void SelectVariant(IModel variant)
214242
{
215243
throw new FoundryLocalException(

sdk/cs/src/FoundryLocalManager.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,4 +460,23 @@ public void Dispose()
460460
Dispose(disposing: true);
461461
GC.SuppressFinalize(this);
462462
}
463+
464+
/// <summary>
465+
/// Get an HTTP client for the OpenAI Responses API.
466+
/// </summary>
467+
/// <remarks>
468+
/// The web service must be started first (see <see cref="StartWebServiceAsync"/>).
469+
/// </remarks>
470+
/// <param name="modelId">Optional default model id used when callers don't supply one.</param>
471+
/// <returns>A new <see cref="OpenAIResponsesClient"/>.</returns>
472+
/// <exception cref="FoundryLocalException">If the web service has not been started.</exception>
473+
public OpenAIResponsesClient GetResponsesClient(string? modelId = null)
474+
{
475+
if (Urls == null || Urls.Length == 0)
476+
{
477+
throw new FoundryLocalException("Web service is not running. Call StartWebServiceAsync first.");
478+
}
479+
480+
return new OpenAIResponsesClient(Urls[0], modelId);
481+
}
463482
}

sdk/cs/src/IModel.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ Task DownloadAsync(Action<float>? downloadProgress = null,
7777
/// <returns>OpenAI.EmbeddingClient</returns>
7878
Task<OpenAIEmbeddingClient> GetEmbeddingClientAsync(CancellationToken? ct = null);
7979

80+
/// <summary>
81+
/// Get an HTTP client for the OpenAI Responses API.
82+
/// </summary>
83+
/// <param name="ct">Optional cancellation token.</param>
84+
/// <returns>An <see cref="OpenAIResponsesClient"/> bound to this model.</returns>
85+
Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null);
86+
8087
/// <summary>
8188
/// Variants of the model that are available. Variants of the model are optimized for different devices.
8289
/// </summary>

0 commit comments

Comments
 (0)