Skip to content

Commit 4731157

Browse files
MaanavDCopilot
andcommitted
Replace SDK Responses API surface with sample + integration tests
This PR pivots away from adding a Responses API client to the C# SDK and instead adds a focused sample and integration tests that exercise the OpenAI Responses API against the Foundry Local web service, mirroring how `samples/cs/foundry-local-web-server` calls chat completions. Reverted on this branch: - `sdk/cs/src/OpenAI/ResponsesClient.cs` and `ResponsesTypes.cs` (deleted) - `ResponsesClientSettings`, factory methods, `IModel.GetResponsesClientAsync`, `Detail/Model.cs` and `Detail/ModelVariant.cs` additions - `OpenAI` PackageReference on the SDK project - `Utils.cs` test-side change - Earlier `ResponsesClientTests.cs` and `ResponsesIntegrationTests.cs` Added: - `samples/cs/responses-foundry-local-web-server/` (Program.cs + csproj) - Loads `qwen2.5-0.5b` via `FoundryLocalManager`, starts the web service - Uses `OpenAI.Responses.ResponsesClient` (official OpenAI .NET package, 2.10.0) pointed at `<service-url>/v1` - Demonstrates non-streaming, streaming (`StreamingResponseOutputTextDeltaUpdate`), and a full function-calling round-trip via `previous_response_id` - `sdk/cs/test/FoundryLocal.Tests/ResponsesIntegrationTests.cs` - Three integration tests: NonStreaming, Streaming, and FunctionCalling round-trip - Skips automatically when `qwen2.5-0.5b` is not in the local cache - Cleans up: stops web service and unloads model in `[After(Class)]` - Bumped centrally-managed `OpenAI` package version 2.5.0 -> 2.10.0 (needed for stable `ResponsesClient`); added `OpenAI 2.10.0` to test project - Updated `samples/cs/README.md` with a row for the new sample Validation: - `dotnet build samples/cs/responses-foundry-local-web-server -c Release`: 0 warnings, 0 errors - `dotnet build sdk/cs/test/FoundryLocal.Tests -c Release`: 0 errors (2 unrelated pre-existing nullable warnings) - Local test run is currently blocked by a pre-existing `GetRepoRoot()` issue in worktrees (`Utils.AssemblyInit` throws when `.git` is a file rather than a directory); affects every test in the project, not Responses-specific Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 072a4bb commit 4731157

15 files changed

Lines changed: 404 additions & 1000 deletions

samples/cs/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
1111
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
1212
<PackageVersion Include="NAudio" Version="2.2.1" />
13-
<PackageVersion Include="OpenAI" Version="2.5.0" />
13+
<PackageVersion Include="OpenAI" Version="2.10.0" />
1414
</ItemGroup>
1515
</Project>

samples/cs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Both packages provide the same APIs, so the same source code works on all platfo
1515
| [embeddings](embeddings/) | Generate single and batch text embeddings using the Foundry Local SDK. |
1616
| [audio-transcription-example](audio-transcription-example/) | Transcribe audio files using the Foundry Local SDK. |
1717
| [foundry-local-web-server](foundry-local-web-server/) | Set up a local OpenAI-compliant web server. |
18+
| [responses-foundry-local-web-server](responses-foundry-local-web-server/) | Use the OpenAI Responses API (non-streaming, streaming, tool calling) against the local web server. |
1819
| [tool-calling-foundry-local-sdk](tool-calling-foundry-local-sdk/) | Use tool calling with native chat completions. |
1920
| [tool-calling-foundry-local-web-server](tool-calling-foundry-local-web-server/) | Use tool calling with the local web server. |
2021
| [model-management-example](model-management-example/) | Manage models, variant selection, and updates. |
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// <complete_code>
2+
// Demonstrates the OpenAI Responses API against the Foundry Local OpenAI-compatible web service.
3+
//
4+
// SDK responsibilities (Foundry Local):
5+
// - SDK initialization
6+
// - EP download/registration
7+
// - model lookup, download, load
8+
// - starting/stopping the local web service
9+
//
10+
// Responses API calls go through the official OpenAI .NET package's `ResponsesClient`
11+
// pointed at the local web service, mirroring how `foundry-local-web-server` uses
12+
// `OpenAIClient.GetChatClient(...)`.
13+
14+
using System.ClientModel;
15+
using System.Text;
16+
using System.Text.Json;
17+
18+
using Microsoft.AI.Foundry.Local;
19+
20+
using OpenAI;
21+
using OpenAI.Responses;
22+
23+
var config = new Configuration
24+
{
25+
AppName = "foundry_local_samples",
26+
LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information,
27+
Web = new Configuration.WebService
28+
{
29+
Urls = "http://127.0.0.1:52495"
30+
}
31+
};
32+
33+
// Initialize the singleton instance.
34+
await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger());
35+
var mgr = FoundryLocalManager.Instance;
36+
37+
// Download and register all execution providers.
38+
var currentEp = "";
39+
await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
40+
{
41+
if (epName != currentEp)
42+
{
43+
if (currentEp != "") Console.WriteLine();
44+
currentEp = epName;
45+
}
46+
Console.Write($"\r {epName.PadRight(30)} {percent,6:F1}%");
47+
});
48+
if (currentEp != "") Console.WriteLine();
49+
50+
// Get the model catalog
51+
var catalog = await mgr.GetCatalogAsync();
52+
53+
// Get a model using an alias
54+
var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found");
55+
56+
// Download the model (the method skips download if already cached)
57+
await model.DownloadAsync(progress =>
58+
{
59+
Console.Write($"\rDownloading model: {progress:F2}%");
60+
if (progress >= 100f)
61+
{
62+
Console.WriteLine();
63+
}
64+
});
65+
66+
// Load the model
67+
Console.Write($"Loading model {model.Id}...");
68+
await model.LoadAsync();
69+
Console.WriteLine("done.");
70+
71+
// Start the web service
72+
Console.Write($"Starting web service on {config.Web.Urls}...");
73+
await mgr.StartWebServiceAsync();
74+
Console.WriteLine("done.");
75+
76+
// <<<<<< OPEN AI RESPONSES SDK USAGE >>>>>>
77+
// Use the OpenAI Responses client to call the local Foundry web service.
78+
ApiKeyCredential key = new ApiKeyCredential("notneeded");
79+
OpenAIClient openai = new OpenAIClient(key, new OpenAIClientOptions
80+
{
81+
Endpoint = new Uri(config.Web.Urls + "/v1"),
82+
});
83+
ResponsesClient responses = openai.GetResponsesClient();
84+
85+
// 1) Non-streaming
86+
Console.WriteLine("\n=== Non-streaming ===");
87+
ResponseResult simple = await responses.CreateResponseAsync(model.Id, "What is 2 + 2? Respond with just the number.");
88+
Console.WriteLine($"[ASSISTANT]: {simple.GetOutputText()}");
89+
90+
// 2) Streaming
91+
Console.WriteLine("\n=== Streaming ===");
92+
Console.Write("[ASSISTANT]: ");
93+
await foreach (StreamingResponseUpdate update in responses.CreateResponseStreamingAsync(model.Id, "Count from 1 to 3."))
94+
{
95+
if (update is StreamingResponseOutputTextDeltaUpdate delta && !string.IsNullOrEmpty(delta.Delta))
96+
{
97+
Console.Write(delta.Delta);
98+
}
99+
}
100+
Console.WriteLine();
101+
102+
// 3) Function/tool calling — full round-trip using previous_response_id.
103+
Console.WriteLine("\n=== Function calling ===");
104+
var weatherSchema = BinaryData.FromString("""
105+
{
106+
"type": "object",
107+
"properties": {
108+
"city": { "type": "string", "description": "The city to look up" }
109+
},
110+
"required": ["city"]
111+
}
112+
""");
113+
114+
var toolOptions = new CreateResponseOptions(
115+
model.Id,
116+
new[] { ResponseItem.CreateUserMessageItem("Use get_weather to look up the weather in Seattle, then summarize it.") })
117+
{
118+
StoredOutputEnabled = true,
119+
ToolChoice = ResponseToolChoice.CreateRequiredChoice(),
120+
};
121+
toolOptions.Tools.Add(ResponseTool.CreateFunctionTool(
122+
functionName: "get_weather",
123+
functionParameters: weatherSchema,
124+
strictModeEnabled: true,
125+
functionDescription: "Get the current weather for a given city."));
126+
127+
ResponseResult toolCallResponse = await responses.CreateResponseAsync(toolOptions);
128+
129+
// Find the function-call output item the model produced.
130+
FunctionCallResponseItem? functionCall = null;
131+
foreach (var item in toolCallResponse.OutputItems)
132+
{
133+
if (item is FunctionCallResponseItem fc && fc.FunctionName == "get_weather")
134+
{
135+
functionCall = fc;
136+
break;
137+
}
138+
}
139+
140+
if (functionCall is null)
141+
{
142+
Console.WriteLine("Model did not produce a function call; skipping tool round-trip.");
143+
}
144+
else
145+
{
146+
var argsJson = functionCall.FunctionArguments?.ToString() ?? "{}";
147+
var city = "unknown";
148+
try
149+
{
150+
city = JsonDocument.Parse(argsJson).RootElement.GetProperty("city").GetString() ?? "unknown";
151+
}
152+
catch (KeyNotFoundException) { /* model gave us no city */ }
153+
154+
Console.WriteLine($"Tool call: get_weather(city=\"{city}\")");
155+
var toolOutput = $$$"""{"city": "{{{city}}}", "temperatureF": 68, "summary": "partly cloudy"}""";
156+
Console.WriteLine($"Tool output: {toolOutput}");
157+
158+
// Submit the tool's output and ask the model to continue using `previous_response_id`.
159+
var followUpOptions = new CreateResponseOptions(
160+
model.Id,
161+
new[] { ResponseItem.CreateFunctionCallOutputItem(functionCall.CallId, toolOutput) })
162+
{
163+
PreviousResponseId = toolCallResponse.Id,
164+
StoredOutputEnabled = true,
165+
};
166+
followUpOptions.Tools.Add(ResponseTool.CreateFunctionTool(
167+
functionName: "get_weather",
168+
functionParameters: weatherSchema,
169+
strictModeEnabled: true,
170+
functionDescription: "Get the current weather for a given city."));
171+
172+
ResponseResult finalResponse = await responses.CreateResponseAsync(followUpOptions);
173+
Console.WriteLine($"[ASSISTANT]: {finalResponse.GetOutputText()}");
174+
}
175+
// <<<<<< END OPEN AI RESPONSES SDK USAGE >>>>>>
176+
177+
// Tidy up
178+
await mgr.StopWebServiceAsync();
179+
await model.UnloadAsync();
180+
// </complete_code>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<!-- OpenAI Responses APIs are experimental in the official OpenAI .NET package. -->
8+
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
9+
</PropertyGroup>
10+
11+
<!-- Windows: target Windows SDK for WinML hardware acceleration -->
12+
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
13+
<TargetFramework>net9.0-windows10.0.26100</TargetFramework>
14+
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
15+
<Platforms>ARM64;x64</Platforms>
16+
<WindowsPackageType>None</WindowsPackageType>
17+
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
18+
</PropertyGroup>
19+
20+
<!-- Non-Windows: standard .NET -->
21+
<PropertyGroup Condition="!$([MSBuild]::IsOSPlatform('Windows'))">
22+
<TargetFramework>net9.0</TargetFramework>
23+
</PropertyGroup>
24+
25+
<PropertyGroup Condition="'$(RuntimeIdentifier)'==''">
26+
<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
27+
</PropertyGroup>
28+
29+
<!-- Windows: WinML for hardware acceleration -->
30+
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
31+
<PackageReference Include="Microsoft.AI.Foundry.Local.WinML" />
32+
</ItemGroup>
33+
34+
<!-- Non-Windows: standard SDK -->
35+
<ItemGroup Condition="!$([MSBuild]::IsOSPlatform('Windows'))">
36+
<PackageReference Include="Microsoft.AI.Foundry.Local" />
37+
</ItemGroup>
38+
39+
<!-- Linux GPU support -->
40+
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
41+
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu" />
42+
<PackageReference Include="Microsoft.ML.OnnxRuntimeGenAI.Cuda" />
43+
</ItemGroup>
44+
45+
<ItemGroup>
46+
<PackageReference Include="OpenAI" />
47+
</ItemGroup>
48+
49+
<!-- Shared utilities -->
50+
<ItemGroup>
51+
<Compile Include="../Shared/*.cs" />
52+
</ItemGroup>
53+
54+
</Project>

sdk/cs/src/Detail/Model.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,6 @@ 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-
112107
public async Task UnloadAsync(CancellationToken? ct = null)
113108
{
114109
await SelectedVariant.UnloadAsync(ct).ConfigureAwait(false);

sdk/cs/src/Detail/ModelVariant.cs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,6 @@ 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-
119112
private async Task<bool> IsLoadedImplAsync(CancellationToken? ct = null)
120113
{
121114
var loadedModels = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false);
@@ -217,27 +210,6 @@ private async Task<OpenAIEmbeddingClient> GetEmbeddingClientImplAsync(Cancellati
217210
return new OpenAIEmbeddingClient(Id);
218211
}
219212

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-
241213
public void SelectVariant(IModel variant)
242214
{
243215
throw new FoundryLocalException(

sdk/cs/src/FoundryLocalManager.cs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -460,23 +460,4 @@ 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-
}
482463
}

sdk/cs/src/IModel.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,6 @@ 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-
8780
/// <summary>
8881
/// Variants of the model that are available. Variants of the model are optimized for different devices.
8982
/// </summary>

sdk/cs/src/Microsoft.AI.Foundry.Local.csproj

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
<!-- This target runs automatically after package assets are resolved and prints the exact version of the Core package that was selected. -->
7373
<Target Name="PrintResolvedVersions" AfterTargets="ResolvePackageAssets">
7474
<Message Importance="High" Text="Resolved Dependencies:" />
75-
<Message Importance="High" Text=" %(PackageDependencies.Identity) : %(PackageDependencies.Version)" Condition="$([System.String]::Copy('%(PackageDependencies.Identity)').StartsWith('Microsoft.AI.Foundry.Local.Core'))" />
75+
<Message Importance="High" Text=" %(PackageDependencies.Identity) : %(PackageDependencies.Version)"
76+
Condition="$([System.String]::Copy('%(PackageDependencies.Identity)').StartsWith('Microsoft.AI.Foundry.Local.Core'))" />
7677
</Target>
7778

7879
<ItemGroup>
@@ -120,13 +121,14 @@
120121
<NoWarn>$(NoWarn);NU1604</NoWarn>
121122
</PropertyGroup>
122123
<ItemGroup>
123-
<PackageReference Condition="'$(UseWinML)' == 'true'" Include="Microsoft.AI.Foundry.Local.Core.WinML" Version="$(FoundryLocalCoreWinMLVersion)" />
124-
<PackageReference Condition="'$(UseWinML)' != 'true'" Include="Microsoft.AI.Foundry.Local.Core" Version="$(FoundryLocalCoreVersion)" />
124+
<PackageReference Condition="'$(UseWinML)' == 'true'"
125+
Include="Microsoft.AI.Foundry.Local.Core.WinML" Version="$(FoundryLocalCoreWinMLVersion)" />
126+
<PackageReference Condition="'$(UseWinML)' != 'true'"
127+
Include="Microsoft.AI.Foundry.Local.Core" Version="$(FoundryLocalCoreVersion)" />
125128

126129
<PackageReference Include="Betalgo.Ranul.OpenAI" Version="9.1.0" />
127130
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.9" />
128131
<!-- specify PrivateAssets to exclude from nuget dependencies -->
129-
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8" PrivateAssets="all" />
130-
<PackageReference Include="OpenAI" Version="2.10.0" />
132+
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8" PrivateAssets="all"/>
131133
</ItemGroup>
132134
</Project>

0 commit comments

Comments
 (0)