feat(sdk/cs): add Responses API client for OpenAI/OpenResponses compat#505
feat(sdk/cs): add Responses API client for OpenAI/OpenResponses compat#505
Conversation
…tibility Add OpenAIResponsesClient to the C# SDK v2 with full CRUD support for the Responses API served by Foundry Local's embedded web service. New files: - src/OpenAI/ResponsesClient.cs: HTTP-based client with SSE streaming - src/OpenAI/ResponsesTypes.cs: Request/response DTOs, items, streaming events - src/OpenAI/ResponsesJsonContext.cs: AOT-compatible source-generated JSON context Modified files: - src/IModel.cs: GetResponsesClientAsync() on IModel interface - src/ModelVariant.cs: Implementation with web service URL validation - src/Model.cs: Delegation to SelectedVariant - src/FoundryLocalManager.cs: GetResponsesClient() factory method Key design decisions: - HTTP-based (HttpClient + SSE), not FFI, since no CoreInterop command exists - AOT-compatible: all serialization uses source-generated JsonSerializerContext - IDisposable: HttpClient properly disposed - Follows existing patterns: Utils.CallWithExceptionHandling, ConfigureAwait(false) - Factory on FoundryLocalManager + convenience on IModel - ResponseObject.OutputText convenience property (matches OpenAI Python SDK) - Full CRUD: Create, CreateStreaming, Get, Delete, Cancel, GetInputItems
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a new HTTP-based C# SDK v2 client for Foundry Local’s embedded web service that implements OpenAI/OpenResponses-compatible Responses API operations (including SSE streaming), and wires it into existing model/manager entry points.
Changes:
- Introduces
OpenAIResponsesClientwith CRUD + streaming overHttpClientand source-generated JSON serialization. - Adds Responses API DTOs + custom JSON converters and a dedicated
ResponsesJsonContextfor AOT/trimming. - Exposes the client via
FoundryLocalManager.GetResponsesClient(...)andIModel.GetResponsesClientAsync()(delegated throughModel/ModelVariant).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk_v2/cs/src/OpenAI/ResponsesTypes.cs | Adds request/response DTOs, polymorphic items/content, and JSON converters for the Responses API. |
| sdk_v2/cs/src/OpenAI/ResponsesJsonContext.cs | Adds a dedicated source-generated JsonSerializerContext for Responses API types (AOT-friendly). |
| sdk_v2/cs/src/OpenAI/ResponsesClient.cs | Implements the HTTP/SSE Responses API client and settings container. |
| sdk_v2/cs/src/IModel.cs | Adds GetResponsesClientAsync() to the public model interface. |
| sdk_v2/cs/src/ModelVariant.cs | Implements GetResponsesClientAsync() with loaded-model + web service checks. |
| sdk_v2/cs/src/Model.cs | Delegates GetResponsesClientAsync() to the selected variant. |
| sdk_v2/cs/src/FoundryLocalManager.cs | Adds a GetResponsesClient(...) factory that requires the web service to be running. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| throw new FoundryLocalException( | ||
| "Model must be specified either in the constructor or via GetResponsesClientAsync(modelId)."); | ||
| } |
There was a problem hiding this comment.
The exception message references GetResponsesClientAsync(modelId), but the manager API added in this PR is FoundryLocalManager.GetResponsesClient(string? modelId = null) and IModel.GetResponsesClientAsync has no modelId parameter. Update the message to reference the correct factory so callers aren’t directed to a non-existent method.
| /// <summary> | ||
| /// Creates an OpenAI Responses API client. | ||
| /// The web service must be started first via <see cref="StartWebServiceAsync"/>. | ||
| /// </summary> | ||
| /// <param name="modelId">Optional default model ID for requests.</param> | ||
| /// <returns>An <see cref="OpenAIResponsesClient"/> instance.</returns> | ||
| /// <exception cref="FoundryLocalException">If the web service is not running.</exception> | ||
| 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); | ||
| } |
There was a problem hiding this comment.
This PR adds a new public client + factory (GetResponsesClient) but there are existing client-focused tests for chat/audio under sdk_v2/cs/test. Consider adding at least basic coverage for the new factory (e.g., throws when Urls is null/empty, succeeds after StartWebServiceAsync) and one smoke test for create/stream parsing so regressions are caught.
| using System.Runtime.CompilerServices; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
| using System.Threading.Channels; |
There was a problem hiding this comment.
using System.Threading.Channels; is unused in this file. With TreatWarningsAsErrors enabled, this will fail the build (CS8019). Remove the unused using directive.
| using System.Threading.Channels; |
| /// Create via <see cref="FoundryLocalManager.GetResponsesClientAsync"/> or | ||
| /// <see cref="IModel.GetResponsesClientAsync"/>. |
There was a problem hiding this comment.
The XML doc cref references FoundryLocalManager.GetResponsesClientAsync, but the manager exposes GetResponsesClient(...) (sync). Invalid cref targets typically produce CS1574/CS1584 during doc generation (and warnings are treated as errors). Update the cref (and surrounding wording) to point at the existing API.
| /// Create via <see cref="FoundryLocalManager.GetResponsesClientAsync"/> or | |
| /// <see cref="IModel.GetResponsesClientAsync"/>. | |
| /// Create via <see cref="FoundryLocalManager.GetResponsesClient"/> or | |
| /// <see cref="IModel.GetResponsesClient"/>. |
| // 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); |
There was a problem hiding this comment.
In StreamingImplAsync, the HttpResponseMessage returned by SendAsync is never disposed. Disposing the StreamReader closes the content stream, but the response object itself (and underlying handlers) should still be disposed to avoid resource/socket leaks—especially for long-lived streaming. Use a try/finally in the async iterator to Dispose() the response when enumeration completes or is cancelled.
| [EnumeratorCancellation] CancellationToken ct) | ||
| { | ||
| var request = BuildRequest(input, stream: true); | ||
| options?.Invoke(request); |
There was a problem hiding this comment.
BuildRequest(..., stream: true) sets Stream = true, but options?.Invoke(request) runs after that and can override request.Stream to false. If that happens, the server may return a non-SSE response while the client still tries to parse SSE, leading to hangs/parse failures. Ensure Stream cannot be overridden for streaming calls (e.g., set request.Stream = true after applying options, or ignore/validate any attempted override).
| options?.Invoke(request); | |
| options?.Invoke(request); | |
| // Ensure streaming stays enabled even if options attempts to override it. | |
| request.Stream = true; |
| /// <summary> | ||
| /// 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 <see cref="FoundryLocalManager.StartWebServiceAsync"/>. | ||
| /// </summary> | ||
| /// <param name="ct">Optional cancellation token.</param> | ||
| /// <returns>OpenAI.ResponsesClient</returns> | ||
| Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null); |
There was a problem hiding this comment.
Adding GetResponsesClientAsync(...) to the public IModel interface is a breaking change for any external implementers of IModel (they will no longer compile). If IModel is intended to be publicly implementable, consider using an extension method or a default interface implementation to preserve compatibility.
Add OpenAIResponsesClient to the C# SDK v2 with full CRUD support for the Responses API served by Foundry Local's embedded web service.
New files:
Modified files:
Key design decisions: