Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step20_DynamicFunctionTools/Agent_Step20_DynamicFunctionTools.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Agent_Step21_ShellWithEnvironment.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step22_MultiModelRouting/Agent_Step22_MultiModelRouting.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.

// Multi-model routing with RoutingChatClient
//
// Demonstrates how to back a single agent with multiple chat clients that use
// different models, and switch between them at runtime.
//
// RoutingChatClient is an IChatClient decorator that holds several named inner
// clients and, for each request, routes to one of them based on the active
// destination stored in the session. It also accepts an optional fallback
// factory that builds a client on the fly for any key that is not a registered
// inner client. Here we key the inner clients (and the fallback) by model name.
//
// This sample:
// 1) Registers two inner clients keyed by model name (models A and B).
// 2) Adds a fallback factory that constructs a Foundry client for whatever
// model name (key) is requested but not pre-registered (model C).
// 3) Retrieves the RoutingChatClient back from the agent via GetService.
// 4) Runs the same agent/session three times, switching the active model
// between runs with SetActiveDestinationKey, so a single conversation is
// served by three different models in turn.
//
// Chat history storage
// --------------------
// Every client is created with AsIChatClientWithStoredOutputDisabled(...), i.e.
// the Responses API "store" flag is set to false and chat history is kept
// client-side by the agent's session. This is required when routing across
// clients: service-stored chat history is tied to the *service* that created the
// conversation, so it is only available when every turn is served by that same
// service. Because routing can send different turns to different clients (and
// potentially different services), the conversation must be carried client-side
// so it is replayed to whichever model handles the next turn.
//
// If you instead route only among clients that all share the same service, you
// can use service-stored history (AsIChatClient(...) with storage enabled) and
// let the service persist the conversation.
//
// Reasoning content
// -----------------
// Every client also passes includeReasoningEncryptedContent: false. Encrypted
// reasoning content is model-specific: one model cannot necessarily interpret
// another model's encrypted reasoning, so it must not be echoed back in requests
// when a single conversation is served by multiple models. When you route to only
// a single model, you can leave this enabled (the default) to preserve reasoning
// across turns.

using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");

// Two pre-registered models (inner clients) and a third model resolved via the
// fallback factory. Set these to models deployed in your Foundry project.
string modelA = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
string modelB = Environment.GetEnvironmentVariable("FOUNDRY_MODEL_ALT1") ?? "gpt-5.4-mini";
string modelC = Environment.GetEnvironmentVariable("FOUNDRY_MODEL_ALT2") ?? "Deepseek-V4-Pro";

// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
var responsesClient = aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClient();

// Register two inner clients keyed by model name. Each uses client-side chat
// history (stored output disabled), and disables encrypted reasoning content
// (includeReasoningEncryptedContent: false) since it is not portable across
// models, so the conversation can move freely between models.
var innerClients = new Dictionary<string, IChatClient>
{
[modelA] = responsesClient.AsIChatClientWithStoredOutputDisabled(modelA, includeReasoningEncryptedContent: false),
[modelB] = responsesClient.AsIChatClientWithStoredOutputDisabled(modelB, includeReasoningEncryptedContent: false),
};

// The fallback factory builds a client for any requested model name (key) that
// is not one of the registered inner clients. The created client is used for the
// single request and then disposed by default. You can disable disposal by setting
// the RoutingChatClientOptions.DisableFallbackChatClientDisposal setting to true.
// This allows you to reuse or cache clients created in the fallback factory,
// but you must then manage their disposal yourself.
var routingChatClient = new RoutingChatClient(
innerClients,
fallbackFactory: (destinationKey, _, _) =>
{
Console.WriteLine($" (fallback factory building a client for model '{destinationKey}')");
return new ValueTask<IChatClient>(responsesClient.AsIChatClientWithStoredOutputDisabled(destinationKey, includeReasoningEncryptedContent: false));
});

AIAgent agent = new ChatClientAgent(
routingChatClient,
instructions: "You are a helpful assistant. Keep answers to a single short sentence, and always start by stating which model you are.",
name: "MultiModelAgent");

AgentSession session = await agent.CreateSessionAsync();

// The RoutingChatClient can be retrieved back from the agent via GetService.
// This is uesful when you don't hold a direct reference to it — for example when
Comment thread
westey-m marked this conversation as resolved.
Outdated
// the agent was created elsewhere or resolved from a DI container.
RoutingChatClient routing = agent.GetService<RoutingChatClient>()
?? throw new InvalidOperationException("The agent is not backed by a RoutingChatClient.");

// Run 1: default destination — the first registered inner client (model A).
Console.WriteLine($"[Active model: {routing.GetActiveDestinationKey(session) ?? "(default)"}]");
Console.WriteLine(await agent.RunAsync("Give me a fun fact about the ocean.", session));
Console.WriteLine();

// Run 2: switch to the second registered inner client (model B).
routing.SetActiveDestinationKey(session, modelB);
Console.WriteLine($"[Active model: {routing.GetActiveDestinationKey(session)}]");
Console.WriteLine(await agent.RunAsync("Give me another one, on a different topic.", session));
Console.WriteLine();

// Run 3: switch to a model that is NOT registered as an inner client. The
// fallback factory constructs a client for it on the fly.
routing.SetActiveDestinationKey(session, modelC);
Console.WriteLine($"[Active model: {routing.GetActiveDestinationKey(session)}]");
Console.WriteLine(await agent.RunAsync("And one more, about space.", session));
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Multi-Model Routing

This sample demonstrates how to back a single agent with multiple chat clients that use different models, using `RoutingChatClient`, and how to switch the active model at runtime.

## What This Sample Shows

`RoutingChatClient` is an `IChatClient` decorator that holds several named inner clients and, for each request, routes to one of them based on the *active destination* stored in the session. It also accepts an optional **fallback factory** that builds a client on the fly for any key that is not a registered inner client.

In this sample the destination key is the **model name**:

- Two inner clients are registered, keyed by model name (models A and B).
- A fallback factory constructs a Foundry client for whatever model name (key) is requested but not pre-registered (model C). The created client serves the single request and is disposed afterwards by default.
- The active model is changed between runs with `SetActiveDestinationKey`, and `GetActiveDestinationKey` reports the current model. A single conversation is served by three different models in turn.
- The `RoutingChatClient` is retrieved back from the agent with `agent.GetService<RoutingChatClient>()` — useful when you don't hold a direct reference (for example when the agent is created elsewhere or resolved from a DI container).

No custom `Router` is supplied, so routing simply follows the per-session active destination — exactly what `SetActiveDestinationKey` controls.

## Chat History Storage

Every client is created with `AsIChatClientWithStoredOutputDisabled(...)`, which sets the Responses API `store` flag to `false` so chat history is kept **client-side** by the agent's session.

This matters when routing across clients:

- **Service-stored chat history** is tied to the **service** that created the conversation, so it is only available when every turn is served by that same service. If your destinations all share one service, you can enable service-stored history (`AsIChatClient(...)`) and let the service persist the conversation.
- **Client-side chat history** (used here) carries the conversation in the agent's session and replays it to whichever model handles the next turn. This is required when routing may send different turns to different clients (and potentially different services), as this sample does.

## Reasoning Content

Every client is also created with `includeReasoningEncryptedContent: false`. Encrypted reasoning content is **model-specific** — one model cannot necessarily interpret another model's encrypted reasoning — so it must not be echoed back in requests when a single conversation is routed across multiple models. If you route to only a single model, you can leave this enabled (the default) to preserve reasoning across turns.

## Prerequisites

- .NET 10 SDK or later
- Microsoft Foundry project endpoint with the models below deployed
- Azure CLI installed and authenticated

**Note**: This sample uses `DefaultAzureCredential`. Sign in with `az login` before running. For production, prefer a specific credential such as `ManagedIdentityCredential`. For more information, see the [Azure CLI authentication documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).

## Environment Variables

```powershell
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project" # Required
$env:FOUNDRY_MODEL="gpt-5.4" # Optional, model A (default: gpt-5.4)
$env:FOUNDRY_MODEL_ALT1="gpt-5.4-mini" # Optional, model B (default: gpt-5.4-mini)
$env:FOUNDRY_MODEL_ALT2="Deepseek-V4-Pro" # Optional, model C, resolved via the fallback factory (default: Deepseek-V4-Pro)
```

Update the model names to match models deployed in your Foundry project.

## Running the Sample

```powershell
cd dotnet/samples/02-agents/Agents/Agent_Step22_MultiModelRouting
dotnet run
```

## Expected Behavior

The sample runs three turns of one conversation:

1. **First turn** — routed to the default destination (model A, the first registered inner client).
2. **Second turn** — `SetActiveDestinationKey` switches to model B (the second registered inner client).
3. **Third turn** — `SetActiveDestinationKey` switches to model C, which is not registered as an inner client, so the fallback factory builds a client for it.

Each response starts by stating which model produced it, and the conversation context carries across all three models because chat history is stored client-side.
2 changes: 2 additions & 0 deletions dotnet/samples/02-agents/Agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Before you begin, ensure you have the following prerequisites:
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline to efficiently limit the size of the conversation history for an agent.|
|[In-function-loop checkpointing](./Agent_Step19_InFunctionLoopCheckpointing/)|This sample demonstrates how to persist chat history after each service call during a tool-calling loop, enabling crash recovery and mid-run observability.|
|[Dynamic function tools](./Agent_Step20_DynamicFunctionTools/)|This sample demonstrates how to dynamically expand the set of function tools available to an agent during a function-calling loop using the ambient FunctionInvocationContext.|
|[Shell tool with environment-aware prompt](./Agent_Step21_ShellWithEnvironment/)|This sample demonstrates using the shell tool in stateless and persistent modes paired with an AIContextProvider that injects environment-aware system prompt instructions.|
|[Multi-model routing](./Agent_Step22_MultiModelRouting/)|This sample demonstrates how to back a single agent with multiple chat clients that use different models via RoutingChatClient, switching the active model at runtime.|

## Running the samples from the console

Expand Down
3 changes: 3 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(BackgroundTaskStatus))]
[JsonSerializable(typeof(List<BackgroundTaskInfo>), TypeInfoPropertyName = "BackgroundTaskInfoList")]

// RoutingChatClient types
[JsonSerializable(typeof(RoutingState))]

[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
}
Loading
Loading