🔀 One gateway, controlled providers

Build a Switchable Multi-Provider AI Gateway with Microsoft.Extensions.AI

Build a .NET 10 ASP.NET Core gateway around IChatClient so application code can use Ollama, OpenAI, an OpenAI-compatible provider such as OpenRouter, or Azure OpenAI through one boundary—while provider endpoints, models, credentials, conversation state, streaming, and tool access stay under server control.

What You Will Build

The finished sample is a small ASP.NET Core Minimal API that accepts a provider name, a message, an optional conversation ID, and an optional tool flag. The HTTP layer never constructs an arbitrary model client from user input. It resolves a provider that the server already knows about, then sends the conversation through IChatClient.

One application boundary

The gateway depends on IChatClient after provider-specific client construction is complete.

Bounded conversations

Conversation history lives in memory, has a message limit, and stays bound to the provider that created it.

Streaming and tools

The same gateway supports complete responses, SSE streaming, and a deterministic temperature-conversion function.

Verified companion sample: the implementation used by this tutorial is published at dotnet-ai/provider-agnostic-chat-gateway. The sample was rebuilt, tested, live-verified with Ollama, and merged through GitHub CI before this page was updated.

Why Provider Abstraction Matters

A provider-specific SDK is useful at the edge of an application. The expensive part is allowing provider-specific types, endpoint rules, credential handling, and model configuration to leak into every endpoint and service that needs AI.

The abstraction does not mean providers become identical. Models and services can differ in tool support, streaming behavior, limits, authentication, and operational characteristics. The sample therefore keeps explicit capability metadata beside each registered client.

The honest promise: IChatClient removes provider-specific dependencies from the gateway logic. It does not erase provider differences, and it does not make every model support every feature.

Prerequisites and Verified Baseline

This tutorial is pinned to the exact implementation that was restored, compiled, tested, and published. It does not use floating 10.x.y placeholders.

ComponentVerified value
Target frameworknet10.0
.NET SDK baseline10.0.401
Solution format.slnx
Microsoft.Extensions.AI10.10.0
Microsoft.Extensions.AI.OpenAI10.10.1
Azure.AI.OpenAI2.1.0
Azure.Identity1.21.0
OllamaSharp5.4.30
Deterministic tests25 passed, 0 failed, 0 skipped
Live verificationOllama / qwen3.8:27b on September 29, 2026
src/ProviderAgnosticChatGateway/ProviderAgnosticChatGateway.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
    <PackageReference Include="Azure.Identity" Version="1.21.0" />
    <PackageReference Include="Microsoft.Extensions.AI" Version="10.10.0" />
    <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.10.1" />
    <PackageReference Include="OllamaSharp" Version="5.4.30" />
  </ItemGroup>
</Project>

You need the .NET 10 SDK. Ollama is required only if you want to reproduce the default local live path. OpenAI, OpenRouter, and Azure OpenAI are optional and disabled in the checked-in configuration.

Why pin the baseline? AI client APIs change. When you intentionally upgrade later, restore, compile, and rerun the deterministic and live checks instead of assuming an older tutorial still matches the new package graph.

Understand IChatClient

IChatClient is the central Microsoft.Extensions.AI chat abstraction. The interface exposes complete-response and streaming-response operations, while provider adapters can wrap concrete SDK clients behind the same contract.

IChatClient

The provider-neutral chat boundary used by ChatGateway.

ChatMessage

The common message representation used to carry conversation history and the current user turn.

ChatOptions

Per-request options such as the function tools offered to the model.

GetResponseAsync

Returns a complete ChatResponse for the supplied message history.

GetStreamingResponseAsync

Streams incremental response updates so the HTTP layer can forward deltas.

ChatClientBuilder

Builds a middleware-style chat pipeline. This sample adds automatic function invocation once during client construction.

Microsoft's documentation also makes an important responsibility clear: the abstraction does not automatically solve prompt injection, message-size limits, authorization, or provider-specific safeguards. Those controls remain application concerns.

Project Architecture

The verified project intentionally uses one web application and one test project. The boundaries are logical rather than spread across extra class libraries.

1

Create the Solution

If you are following from scratch, create a web project and a separate xUnit project. The companion repository already contains these files, so cloning it is faster if your goal is only to run the verified sample.

PowerShell — create the solution
mkdir ProviderAgnosticChatGateway
cd ProviderAgnosticChatGateway

dotnet new globaljson --sdk-version 10.0.401 --roll-forward latestFeature
dotnet new sln -n ProviderAgnosticChatGateway

dotnet new web `
  -n ProviderAgnosticChatGateway `
  -f net10.0 `
  -o .\src\ProviderAgnosticChatGateway

dotnet new xunit `
  -n ProviderAgnosticChatGateway.Tests `
  -f net10.0 `
  -o .\tests\ProviderAgnosticChatGateway.Tests

dotnet sln .\ProviderAgnosticChatGateway.slnx add `
  .\src\ProviderAgnosticChatGateway\ProviderAgnosticChatGateway.csproj

dotnet sln .\ProviderAgnosticChatGateway.slnx add `
  .\tests\ProviderAgnosticChatGateway.Tests\ProviderAgnosticChatGateway.Tests.csproj

dotnet add `
  .\tests\ProviderAgnosticChatGateway.Tests\ProviderAgnosticChatGateway.Tests.csproj `
  reference `
  .\src\ProviderAgnosticChatGateway\ProviderAgnosticChatGateway.csproj

Then add the five direct package references shown in the verified project file above. Keep package versions in source control so a future reader can reproduce the same API surface.

2

Configure Providers

The server configuration separates provider identity from HTTP input. The checked-in default enables only Ollama, while the cloud providers remain disabled until you intentionally configure them.

appsettings.json — gateway and provider configuration
{
  "Gateway": {
    "DefaultProvider": "ollama",
    "AllowProviderSelection": true,
    "MaxMessagesPerConversation": 20,
    "MaxInputCharacters": 8000
  },
  "Providers": {
    "Ollama": {
      "Enabled": true,
      "Endpoint": "http://localhost:11434",
      "Model": "qwen3.8:27b",
      "Capabilities": {
        "Streaming": true,
        "Tools": true,
        "ProviderManagedConversation": false
      }
    },
    "OpenAI": {
      "Enabled": false,
      "Model": "gpt-5-mini",
      "ApiKeyEnvironmentVariable": "OPENAI_API_KEY"
    },
    "OpenRouter": {
      "Enabled": false,
      "Endpoint": "https://openrouter.ai/api/v1",
      "Model": "openai/gpt-5-mini",
      "ApiKeyEnvironmentVariable": "OPENROUTER_API_KEY"
    }
  }
}
Capability metadata is operator-declared. A value such as "Tools": true says the gateway is configured to offer that capability. It is not proof that every model behind that provider will handle tool calls correctly. Test the exact provider/model combination you intend to use.
3

Build Provider Clients at the Application Edge

ProviderClientFactory is where concrete SDK construction belongs. Every factory method returns IChatClient, so provider-specific types stop at this boundary.

Providers/ProviderClientFactory.cs — adapter paths
public static IChatClient CreateOllama(string endpoint, string model)
{
    IChatClient client =
        new OllamaApiClient(new Uri(endpoint), model);

    return AddCommonPipeline(client);
}

public static IChatClient CreateOpenAI(string apiKey, string model)
{
    IChatClient client =
        new OpenAI.Chat.ChatClient(model, apiKey)
            .AsIChatClient();

    return AddCommonPipeline(client);
}

private static IChatClient AddCommonPipeline(IChatClient client) =>
    new ChatClientBuilder(client)
        .UseFunctionInvocation()
        .Build();
ProviderClientFactory.cs — OpenAI-compatible and Azure OpenAI
public static IChatClient CreateOpenAICompatible(
    string endpoint,
    string apiKey,
    string model)
{
    OpenAIClientOptions options =
        new() { Endpoint = new Uri(endpoint) };

    OpenAIClient client =
        new(
            new System.ClientModel.ApiKeyCredential(apiKey),
            options);

    return AddCommonPipeline(
        client.GetChatClient(model).AsIChatClient());
}

public static IChatClient CreateAzureOpenAIWithDefaultCredential(
    string endpoint,
    string deployment)
{
    AzureOpenAIClient client =
        new(new Uri(endpoint), new DefaultAzureCredential());

    return AddCommonPipeline(
        client.GetChatClient(deployment).AsIChatClient());
}

The OpenRouter path uses the OpenAI SDK with a configurable compatible endpoint, then converts its chat client with AsIChatClient(). Azure OpenAI follows the same abstraction boundary after creating its provider-specific client. The common function-invocation pipeline is added once for every provider.

4

Add the Capability-Aware Provider Registry

The registry is both a routing mechanism and a trust boundary. A caller asks for ollama, openai, openrouter, or azure-openai; the request never supplies a raw endpoint or credential.

Providers — capabilities and descriptor
public sealed record ProviderCapabilities(
    bool Streaming,
    bool Tools,
    bool ProviderManagedConversation);

public sealed record ProviderDescriptor(
    string Name,
    IChatClient Client,
    ProviderCapabilities Capabilities);
ProviderRegistry.cs — resolve an allow-listed provider
public ProviderDescriptor Resolve(string? requestedProvider = null)
{
    string selectedProvider =
        string.IsNullOrWhiteSpace(requestedProvider)
            ? DefaultProvider
            : requestedProvider.Trim();

    if (!_allowProviderSelection &&
        !selectedProvider.Equals(
            DefaultProvider,
            StringComparison.OrdinalIgnoreCase))
    {
        throw new InvalidOperationException(
            $"Provider selection is disabled. The gateway uses '{DefaultProvider}'.");
    }

    if (!_providers.TryGetValue(selectedProvider, out ProviderDescriptor? provider))
    {
        throw new KeyNotFoundException(
            $"Provider '{selectedProvider}' is not registered.");
    }

    return provider;
}
Default provider and provider selection are separate settings. You can keep several trusted clients registered while disabling per-request switching, or allow switching only among the names already registered by the server.
5

Manage Bounded Conversation State

The sample manages conversation history itself instead of relying on provider-managed threads. A new conversation receives a random ID and records the provider name that created it. Reusing the ID with another provider is rejected.

ConversationStore.cs — provider binding
if (!existing.ProviderName.Equals(
        providerName.Trim(),
        StringComparison.OrdinalIgnoreCase))
{
    throw new InvalidOperationException(
        $"Conversation '{normalizedId}' is bound to provider " +
        $"'{existing.ProviderName}' and cannot be continued with " +
        $"'{providerName}'.");
}

return existing;
ConversationState.cs — bounded history
internal void AppendMessages(
    IEnumerable<ChatMessage> messages,
    int maxMessages)
{
    lock (_sync)
    {
        _messages.AddRange(messages);

        int removeCount = _messages.Count - maxMessages;
        if (removeCount > 0)
        {
            _messages.RemoveRange(0, removeCount);
        }
    }
}

Each conversation also owns a SemaphoreSlim. The gateway waits on that gate before mutating history, preventing two simultaneous turns from interleaving state in the same conversation.

This is deliberately not durable memory. Restarting the process clears all conversations. The sample also does not implement expiration, distributed locking, Redis, or database persistence.
6

Build the Provider-Neutral Gateway

ChatGateway validates input, resolves the registered provider, obtains the provider-bound conversation, builds a common message list, and invokes the selected IChatClient. The orchestration code does not contain OpenAI, Azure OpenAI, or Ollama request types.

Gateway/ChatRequest.cs
public sealed record ChatRequest(
    string Message,
    string? Provider = null,
    string? ConversationId = null,
    bool UseTools = false);
ChatGateway.cs — non-streaming core
ProviderDescriptor provider =
    ResolveProviderForRequest(request);

ConversationState conversation =
    _conversations.GetOrCreate(
        request.ConversationId,
        provider.Name);

ChatMessage userMessage =
    new(ChatRole.User, request.Message.Trim());

List<ChatMessage> messages =
    [.. conversation.SnapshotMessages(), userMessage];

ChatResponse response =
    await provider.Client.GetResponseAsync(
        messages,
        CreateOptions(request, provider),
        cancellationToken);

conversation.AppendMessages(
    [userMessage, .. response.Messages],
    _conversations.MaxMessages);

The gateway also limits message length using Gateway:MaxInputCharacters. That is intentionally simple, but it demonstrates that input controls belong in the application rather than being delegated to an AI provider.

7

Add a Safe Function Tool

The sample exposes one deterministic, side-effect-free function: convert_temperature. It is intentionally boring. The goal is to verify the Microsoft.Extensions.AI function pipeline without mixing the tutorial with a write-capable business action.

ChatGateway.cs — AIFunction creation
_temperatureTool = AIFunctionFactory.Create(
    (Func<double, string, string, double>)ConvertTemperatureWithLogging,
    name: "convert_temperature",
    description:
        "Convert a temperature value between Celsius, Fahrenheit and Kelvin.");
ChatGateway.cs — offer the tool only when requested
private ChatOptions? CreateOptions(
    ChatRequest request,
    ProviderDescriptor provider)
{
    if (!request.UseTools)
    {
        return null;
    }

    if (!provider.Capabilities.Tools)
    {
        throw new InvalidOperationException(
            $"Provider '{provider.Name}' is not configured for tools.");
    }

    return new ChatOptions
    {
        Tools = [_temperatureTool]
    };
}

The provider clients were already wrapped with UseFunctionInvocation(). When a capable model returns a structured function request, the function-invoking client can execute the corresponding AIFunction, send the result back to the model, and continue until a normal response is produced.

Live proof: the verified Ollama run emitted TOOL CALLED: convert_temperature value=100 from=Celsius to=Fahrenheit result=212 before returning the final model response.
PowerShell — request a tool-enabled response
$toolBody = @{
    message = "Use the convert_temperature tool to convert 100 Celsius to Fahrenheit. Tell me the numeric result."
    provider = "ollama"
    useTools = $true
} | ConvertTo-Json

Invoke-RestMethod `
    -Uri "http://localhost:5073/api/chat" `
    -Method Post `
    -ContentType "application/json" `
    -Body $toolBody |
    ConvertTo-Json -Depth 5
8

Expose the Minimal API Endpoints

The HTTP surface is intentionally small. The provider list lets clients discover what is enabled; chat and streaming use the same ChatRequest contract.

Program.cs — endpoint map
app.MapGet("/healthz", () =>
    Results.Ok(new { status = "ok" }));

app.MapGet(
    "/api/providers",
    (ProviderRegistry providers) =>
        Results.Ok(
            providers.Providers
                .OrderBy(p => p.Name)
                .Select(p => new
                {
                    name = p.Name,
                    isDefault = p.Name.Equals(
                        providers.DefaultProvider,
                        StringComparison.OrdinalIgnoreCase),
                    p.Capabilities
                })));

app.MapPost("/api/chat", HandleChatAsync);
app.MapPost("/api/chat/stream", HandleStreamAsync);
POST /api/chat — request body
{
  "provider": "ollama",
  "conversationId": null,
  "message": "Explain IChatClient in one paragraph.",
  "useTools": false
}

Known client-side errors such as an unknown provider, invalid input, or an invalid conversation/provider combination are translated into HTTP 400 Bad Request. Provider/service failures are not silently rewritten as client errors.

9

Stream Responses with Server-Sent Events

For streaming, the gateway first checks the registered capability metadata, then calls GetStreamingResponseAsync. The HTTP endpoint forwards typed Server-Sent Events and flushes after every event.

Program.cs — SSE writer
context.Response.ContentType = "text/event-stream";
context.Response.Headers["Cache-Control"] = "no-cache";
context.Response.Headers["X-Accel-Buffering"] = "no";

await foreach (
    ChatStreamEvent item
    in gateway.StreamAsync(request, cancellationToken))
{
    string payload = JsonSerializer.Serialize(item);

    await context.Response.WriteAsync(
        $"event: {item.Type}\ndata: {payload}\n\n",
        cancellationToken);

    await context.Response.Body.FlushAsync(cancellationToken);
}

The meta event identifies the conversation and provider, each delta carries incremental text, and done marks completion. The gateway also collects the updates and converts them back to a ChatResponse before appending the assistant turn to conversation history, so a streamed response can participate in the next turn.

PowerShell — inspect the SSE response
$streamBody = @{
    message = "Reply with exactly STREAM_OK"
    provider = "ollama"
    useTools = $false
} | ConvertTo-Json

Invoke-WebRequest `
    -Uri "http://localhost:5073/api/chat/stream" `
    -Method Post `
    -ContentType "application/json" `
    -Body $streamBody `
    -UseBasicParsing |
    Select-Object -ExpandProperty Content

If a client error happens after the stream has started, the endpoint emits an error event instead of trying to replace the response with ordinary JSON.

10

Build and Test Without a Cloud API Key

The companion sample treats live-model behavior and deterministic application behavior as separate verification layers. CI can therefore prove the gateway's routing, state, validation, factory construction, streaming mechanics, and tool code without paying for or depending on a cloud model.

PowerShell / Bash
dotnet restore ProviderAgnosticChatGateway.slnx

dotnet build   ProviderAgnosticChatGateway.slnx   --configuration Release   --no-restore

dotnet test   ProviderAgnosticChatGateway.slnx   --configuration Release   --no-build
Verified result: Release build passed with 0 errors, and the deterministic suite reported 25 total tests, 25 passed, 0 failed, 0 skipped.

The test suite covers provider selection and rejection, capability metadata, provider factories, bounded conversation state, provider binding, request validation, fake IChatClient behavior, streaming, and the deterministic temperature function.

GitHub CI does not run Ollama or a paid provider. The repository workflow restores the solution, builds Release, and runs the deterministic tests only. That keeps CI reproducible and secret-free.
11

Run the Gateway with Ollama

The checked-in default configuration expects Ollama at http://localhost:11434 and uses qwen3.8:27b. If you use another locally installed model, change the trusted server configuration before starting the gateway.

PowerShell — start the gateway
dotnet run --project `
  .\src\ProviderAgnosticChatGateway\ProviderAgnosticChatGateway.csproj

The verified launch listened on http://localhost:5073. Your port can differ if your launch settings or environment differ.

PowerShell — inspect enabled providers
Invoke-RestMethod http://localhost:5073/api/providers |
  ConvertTo-Json -Depth 5
PowerShell — non-streaming chat
$body = @{
    message = "Reply with exactly GATEWAY_OK"
    provider = "ollama"
    useTools = $false
} | ConvertTo-Json

Invoke-RestMethod `
    -Uri "http://localhost:5073/api/chat" `
    -Method Post `
    -ContentType "application/json" `
    -Body $body |
    ConvertTo-Json -Depth 5
Live model output is probabilistic. The verification used short sentinel prompts such as GATEWAY_OK and STREAM_OK to make the transport path easy to inspect. Do not use exact prose equality as a general model-quality test.

What the Verified Run Proved

On September 29, 2026, the published sample was run locally against Ollama with qwen3.8:27b. The verification recorded observable gateway behavior rather than assuming that a successful build implied live-model behavior.

BehaviorObserved result
Provider registrationLIVE VERIFIED — ollama listed as the default provider.
Non-streaming chatLIVE VERIFIED — response contained GATEWAY_OK.
Conversation continuityLIVE VERIFIED — the same conversation ID recalled the earlier GATEWAY_OK turn.
SSE streamingLIVE VERIFIED — deltas reconstructed to STREAM_OK.
Function invocationLIVE VERIFIED — explicit server log recorded the temperature tool call and result 212.
Unknown providerLIVE VERIFIED — request rejected with HTTP 400.

Provider Verification Matrix

Do not turn “the factory compiles” into “every provider was tested live.” The companion sample records three different evidence levels.

1
COMPILE VERIFIED. The provider-specific construction and API path compiles against the pinned package set.
2
DETERMINISTICALLY TESTED. Construction and gateway behavior are tested without making a live provider/model call.
3
LIVE VERIFIED. A real provider/model request completed successfully and observable behavior was recorded.
ProviderConstructionDeterministicLive
OllamaVerifiedVerifiedVerified with qwen3.8:27b
OpenAIVerifiedVerified constructionNot run
OpenRouterVerifiedVerified constructionNot run in this publication run
Azure OpenAIVerifiedVerified constructionNot run
Capability parity is not claimed. Streaming and tool behavior can vary with the exact service and model. The registry's capability values are configuration metadata, not a universal provider certification.

Security and Trust Boundary

The provider registry is not just a convenience abstraction. It prevents the public request from becoming a generic model-proxy configuration surface.

  • Never accept arbitrary provider endpoints from the request body. Otherwise a gateway can become an unintended outbound proxy.
  • Never accept raw provider credentials from ordinary chat requests. Secrets belong in server-side secret stores or environment configuration.
  • Validate input size before model invocation. This sample enforces a maximum input-character count.
  • Keep tools opt-in and capability-aware. A request with useTools=true is rejected if the selected provider is not configured for tools.
  • Treat model output as untrusted application data. Encode it before rendering into HTML and do not treat generated text as authorization.

Microsoft's IChatClient documentation similarly places prompt-injection, message-size, and related safeguards on the application unless a concrete implementation explicitly documents otherwise.

Production Considerations

The sample is intentionally a reference gateway, not a finished multi-tenant AI platform. Its boundaries make later production work easier to add without rewriting the basic provider-neutral path.

  • Authentication and authorization: protect provider choice, expensive models, conversation access, and tool access per user or workload.
  • Conversation lifecycle: add expiration, ownership, quotas, and a distributed store if multiple application instances must share state.
  • Resilience: provider retries, timeouts, circuit breakers, and failover deserve explicit policy. This sample deliberately does not fail over automatically.
  • Observability: Microsoft.Extensions.AI supports chat-client pipelines that can host telemetry and custom middleware. Add redaction rules before logging prompts, responses, or tool arguments.
  • Capability evaluation: validate the exact provider/model pair instead of assuming a provider-wide capability.
  • Tool governance: the tutorial uses a deterministic read-only calculation. Real tools with external side effects need stronger authorization, input validation, audit, and often human approval.
Deliberately excluded from this first gateway sample: Agent Framework, MCP, RAG/vector search, embeddings, persistent conversation storage, Redis, automatic provider failover, load balancing, model ranking, multi-agent orchestration, arbitrary caller-supplied endpoints, write-capable business tools, and production deployment infrastructure.

Common Problems and What They Mean

The app starts with “No AI providers are enabled”

At least one provider must have Enabled=true. The checked-in sample enables Ollama. If you disable it, enable and fully configure another provider before starting the application.

The default provider is configured but not registered

The application intentionally resolves the default provider during startup. A misspelled or disabled default therefore fails fast instead of waiting for the first request.

A cloud provider says its environment variable is missing

That provider was enabled, but the configured API-key environment variable was not present. Either provide the secret through the process environment/secret store or disable the provider. Do not paste the key into appsettings.json.

An existing conversation cannot continue with another provider

That is deliberate. The conversation is bound to the provider that created it. Start a new conversation when switching providers rather than silently replaying history against a different backend.

useTools=true is rejected

The selected provider's registered capability metadata does not allow tools. If you change that metadata, separately verify that the exact model actually supports the tool-calling path.

The streaming request produces several small chunks

That is expected. SSE forwards incremental model output. Consumers should concatenate delta text in order and use the done event as the completion signal.

The Ollama model is not installed

Either pull the configured model or change Providers:Ollama:Model to one already installed. The deterministic tests do not require Ollama at all.

A live response does not match the tutorial word for word

Natural-language model output is not deterministic. Verify transport behavior, provider selection, conversation IDs, SSE framing, function logs, and HTTP status codes rather than expecting identical prose.

Frequently Asked Questions

What is IChatClient in Microsoft.Extensions.AI?

IChatClient is the provider-neutral chat abstraction used by this sample. The gateway calls the same response and streaming methods regardless of whether the registered client came from Ollama, OpenAI, an OpenAI-compatible endpoint, or Azure OpenAI.

Can an HTTP caller supply an arbitrary AI endpoint or API key?

No. The request can select only a provider name that the server already registered. Provider endpoints, models, deployments, and credentials remain trusted server configuration.

Does the sample require a cloud API key to build or test?

No. Restore, Release build, and all 25 deterministic tests run without OpenAI, OpenRouter, or Azure OpenAI credentials. The checked-in default live path uses local Ollama.

Are OpenAI, OpenRouter, and Azure OpenAI live-verified in this tutorial?

No. Their construction paths are compile-verified and deterministically tested in this publication baseline. The live verification on September 29, 2026 used Ollama with qwen3.8:27b.

Is conversation history stored permanently?

No. Conversation history is bounded and kept in memory only. A conversation is also bound to the provider that created it, so the gateway does not silently migrate an existing conversation to another provider.

Does tool calling work with every provider and model?

Not automatically. The gateway stores capability metadata, but actual tool support depends on the selected provider, model, and service configuration. The live tool verification in this tutorial used Ollama with qwen3.8:27b.

Why use a provider registry instead of constructing a client from each request?

The registry turns provider choice into an allow-listed server decision. It keeps endpoints and credentials out of HTTP input, centralizes capability metadata, and lets the rest of the gateway work against IChatClient.

Official Microsoft References

The Microsoft.Extensions.AI concepts used on this page are cross-checked against Microsoft Learn and the public API documentation. The package versions used by the companion sample are pinned separately in the repository.

  1. Microsoft Learn: Microsoft.Extensions.AI libraries
  2. Microsoft Learn: Use the IChatClient interface
  3. Microsoft Learn: AI tool calling in .NET
  4. API reference: IChatClient
  5. API reference: AIFunctionFactory.Create
  6. API reference: FunctionInvokingChatClient
  7. API reference: OpenAI ChatClient to IChatClient adapter

Verified dotnet-guide.com Companion Sample

The complete implementation is maintained in the dotnet-guide.com GitHub tutorial repository. It includes the application source, deterministic tests, architecture notes, provider verification matrix, captured live-run evidence, and machine-readable environment record.

Independent educational sample: dotnet-guide.com is an independent educational publisher and is not affiliated with or endorsed by Microsoft. The gateway architecture and verification terminology on this page describe this reference implementation; they are not Microsoft specifications.