diff --git a/samples/cs/Directory.Packages.props b/samples/cs/Directory.Packages.props
index d799c4cd..db615d6c 100644
--- a/samples/cs/Directory.Packages.props
+++ b/samples/cs/Directory.Packages.props
@@ -10,6 +10,6 @@
-
+
diff --git a/samples/cs/README.md b/samples/cs/README.md
index ad10a3c6..aeb02f25 100644
--- a/samples/cs/README.md
+++ b/samples/cs/README.md
@@ -15,6 +15,7 @@ Both packages provide the same APIs, so the same source code works on all platfo
| [embeddings](embeddings/) | Generate single and batch text embeddings using the Foundry Local SDK. |
| [audio-transcription-example](audio-transcription-example/) | Transcribe audio files using the Foundry Local SDK. |
| [foundry-local-web-server](foundry-local-web-server/) | Set up a local OpenAI-compliant web server. |
+| [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. |
| [tool-calling-foundry-local-sdk](tool-calling-foundry-local-sdk/) | Use tool calling with native chat completions. |
| [tool-calling-foundry-local-web-server](tool-calling-foundry-local-web-server/) | Use tool calling with the local web server. |
| [model-management-example](model-management-example/) | Manage models, variant selection, and updates. |
diff --git a/samples/cs/responses-foundry-local-web-server/Program.cs b/samples/cs/responses-foundry-local-web-server/Program.cs
new file mode 100644
index 00000000..e850bf9c
--- /dev/null
+++ b/samples/cs/responses-foundry-local-web-server/Program.cs
@@ -0,0 +1,174 @@
+//
+// Demonstrates the OpenAI Responses API against the Foundry Local OpenAI-compatible web service.
+//
+// SDK responsibilities (Foundry Local):
+// - SDK initialization
+// - EP download/registration
+// - model lookup, download, load
+// - starting/stopping the local web service
+//
+// Responses API calls go through the official OpenAI .NET package's `ResponsesClient`
+// pointed at the local web service, mirroring how `samples/cs/foundry-local-web-server`
+// uses `OpenAIClient.GetChatClient(...)` for chat completions.
+
+using System.ClientModel;
+
+using Microsoft.AI.Foundry.Local;
+
+using OpenAI;
+using OpenAI.Responses;
+
+var config = new Configuration
+{
+ AppName = "foundry_local_samples",
+ LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information,
+ Web = new Configuration.WebService
+ {
+ Urls = "http://127.0.0.1:52495"
+ }
+};
+
+// Initialize the singleton instance.
+await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger());
+var mgr = FoundryLocalManager.Instance;
+
+// Download and register all execution providers.
+var currentEp = "";
+await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
+{
+ if (epName != currentEp)
+ {
+ if (currentEp != "") Console.WriteLine();
+ currentEp = epName;
+ }
+ Console.Write($"\r {epName.PadRight(30)} {percent,6:F1}%");
+});
+if (currentEp != "") Console.WriteLine();
+
+// Get the model catalog
+var catalog = await mgr.GetCatalogAsync();
+
+// Get a model using an alias
+var model = await catalog.GetModelAsync("qwen2.5-0.5b") ?? throw new Exception("Model not found");
+
+// Download the model (the method skips download if already cached)
+await model.DownloadAsync(progress =>
+{
+ Console.Write($"\rDownloading model: {progress:F2}%");
+ if (progress >= 100f)
+ {
+ Console.WriteLine();
+ }
+});
+
+// Load the model
+Console.Write($"Loading model {model.Id}...");
+await model.LoadAsync();
+Console.WriteLine("done.");
+
+// Start the web service
+Console.Write($"Starting web service on {config.Web.Urls}...");
+await mgr.StartWebServiceAsync();
+Console.WriteLine("done.");
+
+try
+{
+ // <<<<<< OPEN AI RESPONSES SDK USAGE >>>>>>
+ // Use the OpenAI Responses client to call the local Foundry web service.
+ ApiKeyCredential key = new("notneeded");
+ OpenAIClient openai = new(key, new OpenAIClientOptions
+ {
+ Endpoint = new Uri(config.Web.Urls + "/v1"),
+ });
+ ResponsesClient responses = openai.GetResponsesClient();
+
+ // 1) Non-streaming
+ Console.WriteLine("\n=== Non-streaming ===");
+ ResponseResult simple = await responses.CreateResponseAsync(model.Id, "Reply with one short sentence about local AI.");
+ Console.WriteLine($"[ASSISTANT]: {simple.GetOutputText()}");
+
+ // 2) Streaming
+ Console.WriteLine("\n=== Streaming ===");
+ Console.Write("[ASSISTANT]: ");
+ await foreach (StreamingResponseUpdate update in responses.CreateResponseStreamingAsync(model.Id, "Count from 1 to 3."))
+ {
+ if (update is StreamingResponseOutputTextDeltaUpdate delta && !string.IsNullOrEmpty(delta.Delta))
+ {
+ Console.Write(delta.Delta);
+ }
+ }
+ Console.WriteLine();
+
+ // 3) Function/tool calling — full round-trip via previous_response_id.
+ // The function takes no arguments, which matches the pattern small models handle reliably.
+ Console.WriteLine("\n=== Function calling ===");
+ var emptyParamsSchema = BinaryData.FromString("""
+ {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ }
+ """);
+
+ ResponseTool getWeatherTool = ResponseTool.CreateFunctionTool(
+ functionName: "get_weather",
+ functionParameters: emptyParamsSchema,
+ strictModeEnabled: true,
+ functionDescription: "Get the current weather. This sample always returns Seattle weather.");
+
+ var toolCallOptions = new CreateResponseOptions(
+ model.Id,
+ new[] { ResponseItem.CreateUserMessageItem("Use the get_weather tool and then answer with the weather.") })
+ {
+ StoredOutputEnabled = true,
+ ToolChoice = ResponseToolChoice.CreateRequiredChoice(),
+ MaxOutputTokenCount = 64,
+ Temperature = 0.0f,
+ };
+ toolCallOptions.Tools.Add(getWeatherTool);
+
+ ResponseResult toolResponse = await responses.CreateResponseAsync(toolCallOptions);
+
+ FunctionCallResponseItem? functionCall = null;
+ foreach (var item in toolResponse.OutputItems)
+ {
+ if (item is FunctionCallResponseItem fc && fc.FunctionName == "get_weather")
+ {
+ functionCall = fc;
+ break;
+ }
+ }
+
+ if (functionCall is null)
+ {
+ Console.WriteLine("Model did not produce a function call; skipping tool round-trip.");
+ }
+ else
+ {
+ Console.WriteLine($"[TOOL CALL]: {functionCall.FunctionName}({functionCall.FunctionArguments})");
+
+ const string toolOutput = """{"location": "Seattle", "weather": "72 degrees F and sunny"}""";
+
+ var followUpOptions = new CreateResponseOptions(
+ model.Id,
+ new[] { ResponseItem.CreateFunctionCallOutputItem(functionCall.CallId, toolOutput) })
+ {
+ PreviousResponseId = toolResponse.Id,
+ StoredOutputEnabled = true,
+ MaxOutputTokenCount = 64,
+ Temperature = 0.0f,
+ };
+ followUpOptions.Tools.Add(getWeatherTool);
+
+ ResponseResult finalResponse = await responses.CreateResponseAsync(followUpOptions);
+ Console.WriteLine($"[ASSISTANT FINAL]: {finalResponse.GetOutputText()}");
+ }
+ // <<<<<< END OPEN AI RESPONSES SDK USAGE >>>>>>
+}
+finally
+{
+ // Tidy up
+ await mgr.StopWebServiceAsync();
+ await model.UnloadAsync();
+}
+//
diff --git a/samples/cs/responses-foundry-local-web-server/README.md b/samples/cs/responses-foundry-local-web-server/README.md
new file mode 100644
index 00000000..1cf285bb
--- /dev/null
+++ b/samples/cs/responses-foundry-local-web-server/README.md
@@ -0,0 +1,55 @@
+# Foundry Local Responses web service sample (C#)
+
+This sample starts the Foundry Local OpenAI-compatible web service, then uses the official OpenAI .NET SDK to call the Responses API.
+
+The pattern is:
+
+1. `FoundryLocalManager` handles Foundry Local setup, model download/load, web service startup, and cleanup.
+1. `OpenAI.Responses.ResponsesClient` (from the official `OpenAI` NuGet package) handles the actual `/v1/responses` calls.
+
+## Prerequisites
+
+- .NET 9 SDK
+- Internet access on first run to download the sample model
+
+## What the sample does
+
+1. Initializes `FoundryLocalManager`.
+1. Downloads and registers execution providers.
+1. Downloads and loads `qwen2.5-0.5b`.
+1. Starts the local web service at `http://127.0.0.1:52495`.
+1. Creates an `OpenAIClient` pointed at `http://127.0.0.1:52495/v1`.
+1. Runs a non-streaming Responses call.
+1. Runs a streaming Responses call (`StreamingResponseOutputTextDeltaUpdate` events).
+1. Runs a Responses function-calling flow with a sample `get_weather` tool, then submits a tool result back via `previous_response_id`.
+1. Stops the web service and unloads the model.
+
+## Run the sample
+
+```powershell
+cd samples/cs/responses-foundry-local-web-server
+dotnet run
+```
+
+## Expected output
+
+```text
+=== Non-streaming ===
+[ASSISTANT]: 4
+
+=== Streaming ===
+[ASSISTANT]: 1, 2, 3.
+
+=== Function calling ===
+Tool call: get_weather()
+Tool output: {"location": "Seattle", "weather": "72 degrees F and sunny"}
+[ASSISTANT]: It's 72 degrees F and sunny in Seattle.
+```
+
+The exact model text varies.
+
+## Troubleshooting
+
+If the sample fails while creating `FoundryLocalManager` with a native symbol error such as `Failed to resolve 'execute_command_with_binary' symbol`, the installed Foundry Local Core runtime is older than the native bits expect. Try the latest stable `Microsoft.AI.Foundry.Local[.WinML]` package, or a recent ORT-Nightly package if needed.
+
+If port `52495` is already in use, edit `Program.cs` and change `config.Web.Urls`.
diff --git a/samples/cs/responses-foundry-local-web-server/ResponsesFoundryLocalWebServer.csproj b/samples/cs/responses-foundry-local-web-server/ResponsesFoundryLocalWebServer.csproj
new file mode 100644
index 00000000..a37de5c1
--- /dev/null
+++ b/samples/cs/responses-foundry-local-web-server/ResponsesFoundryLocalWebServer.csproj
@@ -0,0 +1,54 @@
+
+
+
+ Exe
+ enable
+ enable
+
+ $(NoWarn);OPENAI001
+
+
+
+
+ net9.0-windows10.0.26100
+ false
+ ARM64;x64
+ None
+ false
+
+
+
+
+ net9.0
+
+
+
+ $(NETCoreSdkRuntimeIdentifier)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
index fe0dfcd2..2c41305b 100644
--- a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
+++ b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
@@ -8,6 +8,8 @@
true
false
false
+
+ $(NoWarn);OPENAI001
@@ -47,6 +49,7 @@
+
diff --git a/sdk/cs/test/FoundryLocal.Tests/ResponsesIntegrationTests.cs b/sdk/cs/test/FoundryLocal.Tests/ResponsesIntegrationTests.cs
new file mode 100644
index 00000000..3fb150c0
--- /dev/null
+++ b/sdk/cs/test/FoundryLocal.Tests/ResponsesIntegrationTests.cs
@@ -0,0 +1,241 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Microsoft.AI.Foundry.Local.Tests;
+
+using System;
+using System.ClientModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using OfficialOpenAI = global::OpenAI;
+using OfficialResponses = global::OpenAI.Responses;
+
+///
+/// Integration tests for the OpenAI Responses API served by the Foundry Local web service.
+///
+/// The Foundry Local SDK is responsible only for model lifecycle and starting the local
+/// web service. The Responses API is exercised through the official OpenAI .NET package
+/// pointed at the local /v1 endpoint.
+///
+/// Mirrors sdk/js/test/openai/responsesWebService.test.ts from the JS PR:
+/// - Skips when running in CI.
+/// - Skips when qwen2.5-0.5b is not in the local cache.
+/// - Covers non-streaming, streaming, and a full function-calling round-trip.
+///
+internal sealed class ResponsesIntegrationTests
+{
+ private const string ModelAlias = "qwen2.5-0.5b";
+ private const string ModelVariant = "qwen2.5-0.5b-instruct-generic-cpu:4";
+
+ private static IModel? model;
+ private static OfficialOpenAI.OpenAIClient? openAiClient;
+ private static OfficialResponses.ResponsesClient? responses;
+ private static string? skipReason;
+
+ [Before(Class)]
+ public static async Task Setup()
+ {
+ if (Utils.IsRunningInCI())
+ {
+ skipReason = "Responses integration tests require a local model cache; skipped in CI.";
+ return;
+ }
+
+ var manager = FoundryLocalManager.Instance; // initialized by Utils
+
+ var catalog = await manager.GetCatalogAsync();
+ var modelVariant = await catalog.GetModelVariantAsync(ModelVariant).ConfigureAwait(false);
+ if (modelVariant is null)
+ {
+ skipReason = $"Model variant '{ModelVariant}' is not in the catalog.";
+ return;
+ }
+
+ if (!await modelVariant.IsCachedAsync().ConfigureAwait(false))
+ {
+ skipReason = $"Model '{ModelAlias}' is not cached locally; pre-cache via the SDK to enable these tests.";
+ return;
+ }
+
+ await modelVariant.LoadAsync().ConfigureAwait(false);
+ await Assert.That(await modelVariant.IsLoadedAsync()).IsTrue();
+ model = modelVariant;
+
+ await manager.StartWebServiceAsync().ConfigureAwait(false);
+ await Assert.That(manager.Urls).IsNotNull().And.IsNotEmpty();
+
+ var endpoint = new Uri(manager.Urls![0].TrimEnd('/') + "/v1");
+ openAiClient = new OfficialOpenAI.OpenAIClient(
+ new ApiKeyCredential("notneeded"),
+ new OfficialOpenAI.OpenAIClientOptions { Endpoint = endpoint });
+ responses = openAiClient.GetResponsesClient();
+ }
+
+ [After(Class)]
+ public static async Task TearDown()
+ {
+ var manager = FoundryLocalManager.Instance;
+ try
+ {
+ await manager.StopWebServiceAsync().ConfigureAwait(false);
+ }
+ catch
+ {
+ // best-effort cleanup
+ }
+
+ if (model is not null)
+ {
+ try
+ {
+ await model.UnloadAsync().ConfigureAwait(false);
+ }
+ catch
+ {
+ // best-effort cleanup
+ }
+ }
+ }
+
+ private static (OfficialResponses.ResponsesClient Client, IModel Model) RequireSetup()
+ {
+ if (skipReason is not null || responses is null || model is null)
+ {
+ Skip.Test(skipReason ?? "Responses integration setup did not complete.");
+ }
+
+ return (responses!, model!);
+ }
+
+ [Test]
+ public async Task NonStreaming_SimplePrompt_ReturnsCompletedResponseWithText()
+ {
+ var (client, m) = RequireSetup();
+
+ var options = new OfficialResponses.CreateResponseOptions(
+ m.Id,
+ new[] { OfficialResponses.ResponseItem.CreateUserMessageItem("What is 2 + 2? Answer with just the number.") })
+ {
+ Temperature = 0.0f,
+ MaxOutputTokenCount = 64,
+ StoredOutputEnabled = false,
+ };
+
+ OfficialResponses.ResponseResult response = await client.CreateResponseAsync(options).ConfigureAwait(false);
+
+ await Assert.That(response).IsNotNull();
+ await Assert.That(response.Status).IsEqualTo(OfficialResponses.ResponseStatus.Completed);
+
+ var text = response.GetOutputText();
+ Console.WriteLine($"[NonStreaming] {text}");
+ await Assert.That(text).IsNotNull().And.IsNotEmpty();
+ }
+
+ [Test]
+ public async Task Streaming_EmitsCreatedDeltaAndCompletedEvents()
+ {
+ var (client, m) = RequireSetup();
+
+ var options = new OfficialResponses.CreateResponseOptions(
+ m.Id,
+ new[] { OfficialResponses.ResponseItem.CreateUserMessageItem("Count from 1 to 3.") })
+ {
+ Temperature = 0.0f,
+ MaxOutputTokenCount = 64,
+ StoredOutputEnabled = false,
+ StreamingEnabled = true,
+ };
+
+ var sawCreated = false;
+ var sawTextDelta = false;
+ var sawCompleted = false;
+ var aggregate = new StringBuilder();
+
+ await foreach (OfficialResponses.StreamingResponseUpdate update in client.CreateResponseStreamingAsync(options).ConfigureAwait(false))
+ {
+ switch (update)
+ {
+ case OfficialResponses.StreamingResponseCreatedUpdate:
+ sawCreated = true;
+ break;
+ case OfficialResponses.StreamingResponseOutputTextDeltaUpdate delta when !string.IsNullOrEmpty(delta.Delta):
+ sawTextDelta = true;
+ aggregate.Append(delta.Delta);
+ break;
+ case OfficialResponses.StreamingResponseCompletedUpdate:
+ sawCompleted = true;
+ break;
+ }
+ }
+
+ Console.WriteLine($"[Streaming] aggregated: {aggregate}");
+ await Assert.That(sawCreated).IsTrue();
+ await Assert.That(sawTextDelta).IsTrue();
+ await Assert.That(sawCompleted).IsTrue();
+ }
+
+ [Test]
+ public async Task FunctionCalling_FullRoundTrip_ProducesAssistantText()
+ {
+ var (client, m) = RequireSetup();
+
+ var emptyParamsSchema = BinaryData.FromString("""
+ {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ }
+ """);
+
+ OfficialResponses.ResponseTool getWeatherTool = OfficialResponses.ResponseTool.CreateFunctionTool(
+ functionName: "get_weather",
+ functionParameters: emptyParamsSchema,
+ strictModeEnabled: true,
+ functionDescription: "Get the current weather. This test always returns Seattle weather.");
+
+ var initialOptions = new OfficialResponses.CreateResponseOptions(
+ m.Id,
+ new[] { OfficialResponses.ResponseItem.CreateUserMessageItem("Use the get_weather tool and then answer with the weather.") })
+ {
+ ToolChoice = OfficialResponses.ResponseToolChoice.CreateRequiredChoice(),
+ Temperature = 0.0f,
+ MaxOutputTokenCount = 64,
+ StoredOutputEnabled = true,
+ };
+ initialOptions.Tools.Add(getWeatherTool);
+
+ OfficialResponses.ResponseResult toolResponse = await client.CreateResponseAsync(initialOptions).ConfigureAwait(false);
+
+ var functionCall = toolResponse.OutputItems
+ .OfType()
+ .FirstOrDefault(item => item.FunctionName == "get_weather");
+ await Assert.That(functionCall).IsNotNull();
+ await Assert.That(functionCall!.CallId).IsNotNull().And.IsNotEmpty();
+
+ const string toolOutput = """{"location": "Seattle", "weather": "72 degrees F and sunny"}""";
+
+ var followUpOptions = new OfficialResponses.CreateResponseOptions(
+ m.Id,
+ new[] { OfficialResponses.ResponseItem.CreateFunctionCallOutputItem(functionCall.CallId, toolOutput) })
+ {
+ PreviousResponseId = toolResponse.Id,
+ Temperature = 0.0f,
+ MaxOutputTokenCount = 64,
+ StoredOutputEnabled = false,
+ };
+ followUpOptions.Tools.Add(getWeatherTool);
+
+ OfficialResponses.ResponseResult finalResponse = await client.CreateResponseAsync(followUpOptions).ConfigureAwait(false);
+
+ await Assert.That(finalResponse.Status).IsEqualTo(OfficialResponses.ResponseStatus.Completed);
+
+ var finalText = finalResponse.GetOutputText();
+ Console.WriteLine($"[FunctionCalling] tool_call_id={functionCall.CallId} final={finalText}");
+ await Assert.That(finalText).IsNotNull().And.IsNotEmpty();
+ }
+}
diff --git a/sdk/cs/test/FoundryLocal.Tests/Utils.cs b/sdk/cs/test/FoundryLocal.Tests/Utils.cs
index a289011b..539cbf44 100644
--- a/sdk/cs/test/FoundryLocal.Tests/Utils.cs
+++ b/sdk/cs/test/FoundryLocal.Tests/Utils.cs
@@ -435,7 +435,9 @@ private static List BuildTestCatalog(bool includeCuda = true)
private static string GetSourceFilePath([CallerFilePath] string path = "") => path;
- // Gets the root directory of the foundry-local-sdk repository by finding the .git directory.
+ // Gets the root directory of the foundry-local-sdk repository by finding the .git entry.
+ // In a regular clone the .git entry is a directory; in a worktree it is a file containing
+ // a `gitdir:` pointer. Accept either so tests can run from worktrees.
private static string GetRepoRoot()
{
var sourceFile = GetSourceFilePath();
@@ -443,7 +445,8 @@ private static string GetRepoRoot()
while (dir != null)
{
- if (Directory.Exists(Path.Combine(dir.FullName, ".git")))
+ var gitPath = Path.Combine(dir.FullName, ".git");
+ if (Directory.Exists(gitPath) || File.Exists(gitPath))
return dir.FullName;
dir = dir.Parent;