Skip to content

feat(sdk/cs): add Responses API client for OpenAI/OpenResponses compat#505

Open
MaanavD wants to merge 1 commit intomainfrom
feature/cs-sdk-responses-api
Open

feat(sdk/cs): add Responses API client for OpenAI/OpenResponses compat#505
MaanavD wants to merge 1 commit intomainfrom
feature/cs-sdk-responses-api

Conversation

@MaanavD
Copy link
Collaborator

@MaanavD MaanavD commented Mar 10, 2026

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

…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
@vercel
Copy link

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
foundry-local Ready Ready Preview, Comment Mar 10, 2026 5:42pm

Request Review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OpenAIResponsesClient with CRUD + streaming over HttpClient and source-generated JSON serialization.
  • Adds Responses API DTOs + custom JSON converters and a dedicated ResponsesJsonContext for AOT/trimming.
  • Exposes the client via FoundryLocalManager.GetResponsesClient(...) and IModel.GetResponsesClientAsync() (delegated through Model/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.

Comment on lines +365 to +367
throw new FoundryLocalException(
"Model must be specified either in the constructor or via GetResponsesClientAsync(modelId).");
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +169
/// <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);
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading.Channels;
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using System.Threading.Channels; is unused in this file. With TreatWarningsAsErrors enabled, this will fail the build (CS8019). Remove the unused using directive.

Suggested change
using System.Threading.Channels;

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +26
/// Create via <see cref="FoundryLocalManager.GetResponsesClientAsync"/> or
/// <see cref="IModel.GetResponsesClientAsync"/>.
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// Create via <see cref="FoundryLocalManager.GetResponsesClientAsync"/> or
/// <see cref="IModel.GetResponsesClientAsync"/>.
/// Create via <see cref="FoundryLocalManager.GetResponsesClient"/> or
/// <see cref="IModel.GetResponsesClient"/>.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +243
// 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);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
[EnumeratorCancellation] CancellationToken ct)
{
var request = BuildRequest(input, stream: true);
options?.Invoke(request);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
options?.Invoke(request);
options?.Invoke(request);
// Ensure streaming stays enabled even if options attempts to override it.
request.Stream = true;

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +78
/// <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);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants