Microsoft.Extensions.AI의 IChatClient 인터페이스를 구현하는 주요 AI 클라이언트들과 모두 호환될 수 있게 디자인되어 있기 때문에, Azure와 OpenAI는 물론, Bedrock, Google Vertex 등 다른 모든 AI 클라이언트와도 상호 작용이 가능합니다.
개인적으로는 테스트 단계에서 비용을 지출하지 않는 것을 선호하기 때문에 OpenRouter의 무료 버전 모델을 사용하는 것을 무척 중요하게 생각하는데, 다행이 잘 지원됩니다. 아래는 간단한 예제 코드입니다.
var collections = new ServiceCollection();
collections.AddHttpClient("OpenRouter")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
.AddHttpMessageHandler(() => new OpenRouterHeaderManipHandler())
.AddDefaultLogger();
var services = collections.BuildServiceProvider();
var clientFactory = services.GetRequiredService<IHttpClientFactory>();
var url = new Uri("https://openrouter.ai/api/v1/", UriKind.Absolute);
var credential = new ApiKeyCredential(Util.GetPassword("openai-testkey"));
var transport = new HttpClientPipelineTransport(clientFactory.CreateClient("OpenRouter"));
var chatClient = new OpenAIClient(credential, new OpenAIClientOptions
{
Endpoint = url,
Transport = transport,
});
var agent = new ChatClientAgent(
chatClient.GetChatClient("openai/gpt-oss-20b:free").AsIChatClient(),
instructions: "You are good at telling jokes.",
name: "Joker");
var result = await agent.RunAsync();
foreach (var eachMessage in result.Messages)
Console.WriteLine(eachMessage.ToString());
public sealed class OpenRouterHeaderManipHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var openaiBetaHeader = "OpenAI-Beta";
if (request.Headers.Contains(openaiBetaHeader))
request.Headers.Remove(openaiBetaHeader);
return base.SendAsync(request, cancellationToken);
}
}
위의 코드는 Microsoft.Agents.AI (Preview), Microsoft.Extensions.AI.OpenAI (Preview), Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Http 4개의 nuget 패키지가 필요합니다.
Agent Framework를 사용하는 더 확실한 이유는 역시 Workflow일 것입니다.
#!/usr/bin/env dotnet
#:package Microsoft.Agents.AI.Workflows@1.0.0-preview.251007.1
#:package Microsoft.Extensions.AI.OpenAI@9.9.1-preview.1.25474.6
#:package Microsoft.Extensions.DependencyInjection@9.0.*
#:package Microsoft.Extensions.Http@9.0.*
#:property PublishAot=False
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI;
using System.ClientModel;
using System.ClientModel.Primitives;
// https://devblogs.microsoft.com/dotnet/introducing-microsoft-agent-framework-preview/
var collections = new ServiceCollection();
collections.AddHttpClient("OpenRouter")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
.AddHttpMessageHandler(() => new OpenRouterHeaderManipHandler())
.AddDefaultLogger();
var services = collections.BuildServiceProvider();
var clientFactory = services.GetRequiredService<IHttpClientFactory>();
var openRouterApiKey = Environment.GetEnvironmentVariable("OPENROUTER_APIKEY") ??
throw new Exception("OPENROUTER_APIKEY is required.");
var openRouterModel = Environment.GetEnvironmentVariable("OPENROUTER_MODEL") ??
"openai/gpt-oss-20b:free";
var url = new Uri("https://openrouter.ai/api/v1/", UriKind.Absolute);
var credential = new ApiKeyCredential(openRouterApiKey);
var transport = new HttpClientPipelineTransport(clientFactory.CreateClient("OpenRouter"));
var chatClient = new OpenAIClient(credential, new OpenAIClientOptions
{
Endpoint = url,
Transport = transport,
});
var writer = new ChatClientAgent(
chatClient.GetChatClient(openRouterModel).AsIChatClient(),
name: "Writer",
instructions: "Write stories that are engaging and creative.");
var editor = new ChatClientAgent(
chatClient.GetChatClient(openRouterModel).AsIChatClient(),
instructions: "Make the story more engaging, fix grammar, and enhance the plot.",
name: "Editor");
var workflow = AgentWorkflowBuilder.BuildSequential(writer, editor);
var workflowAgent = await workflow.AsAgentAsync(name: "StoryWriters");
var result = await workflowAgent.RunAsync("Write a short story about a haunted house.");
Console.WriteLine(result.Text);
public sealed class OpenRouterHeaderManipHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var openaiBetaHeader = "OpenAI-Beta";
if (request.Headers.Contains(openaiBetaHeader))
request.Headers.Remove(openaiBetaHeader);
return base.SendAsync(request, cancellationToken);
}
}