Skip to content

Commit 1b00229

Browse files
committed
feat(sdk/cs): add Responses API client for OpenAI/OpenResponses 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: - 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
1 parent b76b3ea commit 1b00229

8 files changed

Lines changed: 1703 additions & 0 deletions

File tree

sdk/cs/src/FoundryLocalManager.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ await Utils.CallWithExceptionHandling(() => EnsureEpsDownloadedImplAsync(ct),
150150
.ConfigureAwait(false);
151151
}
152152

153+
/// <summary>
154+
/// Creates an OpenAI Responses API client.
155+
/// The web service must be started first via <see cref="StartWebServiceAsync"/>.
156+
/// </summary>
157+
/// <param name="modelId">Optional default model ID for requests.</param>
158+
/// <returns>An <see cref="OpenAIResponsesClient"/> instance.</returns>
159+
/// <exception cref="FoundryLocalException">If the web service is not running.</exception>
160+
public OpenAIResponsesClient GetResponsesClient(string? modelId = null)
161+
{
162+
if (Urls == null || Urls.Length == 0)
163+
{
164+
throw new FoundryLocalException(
165+
"Web service is not running. Call StartWebServiceAsync before creating a ResponsesClient.", _logger);
166+
}
167+
168+
return new OpenAIResponsesClient(Urls[0], modelId);
169+
}
170+
153171
private FoundryLocalManager(Configuration configuration, ILogger logger)
154172
{
155173
_config = configuration ?? throw new ArgumentNullException(nameof(configuration));

sdk/cs/src/IModel.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,14 @@ Task DownloadAsync(Action<float>? downloadProgress = null,
6767
/// <param name="ct">Optional cancellation token.</param>
6868
/// <returns>OpenAI.AudioClient</returns>
6969
Task<OpenAIAudioClient> GetAudioClientAsync(CancellationToken? ct = null);
70+
71+
/// <summary>
72+
/// Get an OpenAI Responses API client.
73+
/// Unlike Chat/Audio clients (which use FFI), the Responses API is HTTP-based,
74+
/// so the web service must be started first via <see cref="FoundryLocalManager.StartWebServiceAsync"/>.
75+
/// </summary>
76+
/// <param name="ct">Optional cancellation token.</param>
77+
/// <returns>OpenAI.ResponsesClient</returns>
78+
Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null)
79+
=> throw new NotImplementedException("GetResponsesClientAsync is not implemented by this IModel provider.");
7080
}

sdk/cs/src/Model.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ public async Task<OpenAIAudioClient> GetAudioClientAsync(CancellationToken? ct =
113113
return await SelectedVariant.GetAudioClientAsync(ct).ConfigureAwait(false);
114114
}
115115

116+
public async Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null)
117+
{
118+
return await SelectedVariant.GetResponsesClientAsync(ct).ConfigureAwait(false);
119+
}
120+
116121
public async Task UnloadAsync(CancellationToken? ct = null)
117122
{
118123
await SelectedVariant.UnloadAsync(ct).ConfigureAwait(false);

sdk/cs/src/ModelVariant.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ public async Task<OpenAIAudioClient> GetAudioClientAsync(CancellationToken? ct =
100100
.ConfigureAwait(false);
101101
}
102102

103+
public async Task<OpenAIResponsesClient> GetResponsesClientAsync(CancellationToken? ct = null)
104+
{
105+
return await Utils.CallWithExceptionHandling(() => GetResponsesClientImplAsync(ct),
106+
"Error getting responses client for model", _logger)
107+
.ConfigureAwait(false);
108+
}
109+
103110
private async Task<bool> IsLoadedImplAsync(CancellationToken? ct = null)
104111
{
105112
var loadedModels = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false);
@@ -190,4 +197,21 @@ private async Task<OpenAIAudioClient> GetAudioClientImplAsync(CancellationToken?
190197

191198
return new OpenAIAudioClient(Id);
192199
}
200+
201+
private async Task<OpenAIResponsesClient> GetResponsesClientImplAsync(CancellationToken? ct = null)
202+
{
203+
if (!await IsLoadedAsync(ct))
204+
{
205+
throw new FoundryLocalException($"Model {Id} is not loaded. Call LoadAsync first.");
206+
}
207+
208+
var urls = FoundryLocalManager.Instance.Urls;
209+
if (urls == null || urls.Length == 0)
210+
{
211+
throw new FoundryLocalException(
212+
"Web service is not running. Call StartWebServiceAsync before creating a ResponsesClient.");
213+
}
214+
215+
return new OpenAIResponsesClient(urls[0], Id);
216+
}
193217
}

0 commit comments

Comments
 (0)