用 C# 把自定义模型接入 GitHub Copilot SDK
嗨,我知道,这个博客总绕不开同一个主题:agent harness(智能体运行框架)。在《用 C# 打造你自己的 AI Agent Harness》里,我们从零构建了一个;在《从 dotnet run 到 Foundry Hosted Agent,只需 3 行 C#》里,我们把一个 agent 托管了起来。今天我们要用的 harness 完全不用自己搭:GitHub 已经通过 GitHub Copilot SDK 提供了现成的运行时。
在 Let's Learn GitHub Copilot SDK 系列课程中,我们收到最多的问题之一就是:“我能把 Copilot SDK 和我自己部署的模型一起用吗?”简短回答是:可以。展开说,GitHub Copilot SDK 为你的应用提供 Copilot 智能体运行时,而 ProviderConfig 让你把模型推理路由到你控制的提供商。这个提供商可以是 Copilot SDK 官方支持的任何一家:OpenAI、Anthropic、Ollama、Microsoft Foundry Local,或者任何兼容 OpenAI 接口的端点,比如 vLLM 或 LiteLLM。
今天的示例用的是 Microsoft Foundry,通过 Microsoft Entra ID 认证,必要时也可以用 API 密钥。完整示例现在已经并入 Generative AI for Beginners .NET。它是一个很小的 .NET 10 控制台应用:读取配置、创建 BYOK 提供商、启动 Copilot 运行时、创建一个会话,然后提一个问题。
这个示例里的 BYOK 是什么意思
BYOK 指 Bring Your Own Key,也就是“自带密钥”。但在当前语境下,重点不只是密钥本身,而是要更宽泛一些:你自带模型提供商。Copilot SDK 仍然提供运行时,负责管理客户端、会话、系统消息和智能体配置。ProviderConfig 改变的只是模型推理的目标地址和认证方式。
BYOK 不是客户管理的加密密钥
在本文中,BYOK 指的是用你自己的模型端点和凭据来做推理,跟客户管理的加密密钥(customer-managed encryption keys)或存储加密配置没有关系。
这个示例刻意把会话保持得极简:关闭了工具、技能、配置发现和宿主 Git 操作,好让首次运行只清晰地演示一件事——Copilot SDK 会话可以把模型请求发给你自己的、兼容 Foundry 的部署。
运行时和模型提供者是应用里两个相互独立的部分。示例项目是一个普通的 .NET 10 控制台应用,用到下面这些包:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>genai-beginners-dotnet</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="GitHub.Copilot.SDK" Version="1.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.3" />
</ItemGroup>
</Project>
GitHub.Copilot.SDK 提供 Copilot 运行时的 .NET 客户端。Azure.Identity 提供 AzureCliCredential,让示例能够复用 az login 已经拿到的身份。两个配置包用来加载仅用于开发环境的密钥,避免把终结点或凭据写进源码仓库。
第 1 步:读取模型配置
示例复用了仓库里已有的共享 User Secrets 配置:
// User Secrets 把终结点和凭据保存在仓库之外。
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>(optional: true)
.Build();
// 本示例复用仓库里共享的 AzureOpenAI 密钥。
var modelName = GetRequiredValue(configuration, "AzureOpenAI:Deployment");
var endpoint = GetRequiredValue(configuration, "AzureOpenAI:Endpoint");
var apiKey = configuration["AzureOpenAI:ApiKey"]?.Trim();
必填项有两个:
AzureOpenAI:Deployment——你在 Microsoft Foundry 或 Azure OpenAI 中创建的模型部署名称。请选择支持 Responses API 的部署,因为本示例使用的是WireApi = "responses"。AzureOpenAI:Endpoint——Foundry 或 Azure OpenAI 资源的终结点。
AzureOpenAI:ApiKey 是可选项。如果没有提供,示例会通过 AzureCliCredential 走 Microsoft Entra ID 认证;如果提供了,示例会把它放进 api-key 请求头发送。
第 2 步:配置提供程序
这是整个示例的核心:
// Microsoft Foundry 提供的 API 与 OpenAI Responses 兼容。
var provider = new ProviderConfig
{
Type = "openai",
BaseUrl = NormalizeEndpoint(endpoint),
WireApi = "responses",
ModelId = modelName,
WireModel = modelName,
MaxPromptTokens = 8000,
MaxOutputTokens = 512
};
这段配置告诉 Copilot 运行时如何与提供方通信:
Type = "openai"选择 OpenAI 兼容的提供方形态;BaseUrl指向兼容 OpenAI 的 API 地址;WireApi = "responses"选择 Responses API;ModelId是 Copilot 运行时用来识别模型的标识;WireModel是发送给提供方的模型名;- 两个 token 上限让这个小例子保持可控规模。
如果用的是推理模型,MaxOutputTokens 不只要算可见回答的 token,还要算上推理过程消耗的 token。512 这个上限很可能在模型给出任何答案之前就被用完。使用推理部署时,建议调大这个值或直接去掉 MaxOutputTokens 覆盖,再根据自己的负载来调整。
这个示例里 ModelId 和 WireModel 取值相同,但它们分成两个属性,是因为面向运行时的模型标识和面向提供方的模型名在不同集成中不一定相同。
端点辅助方法既可以接受资源 URL,也可以接受已经带上 /openai/v1/ 的 URL:
static string NormalizeEndpoint(string endpoint)
{
var candidate = endpoint.Trim();
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri) ||
uri.Scheme != Uri.UriSchemeHttps ||
string.IsNullOrWhiteSpace(uri.Host))
{
throw new ArgumentException("The configured AzureOpenAI:Endpoint must be an HTTPS URL.");
}
// 既接受资源 URL,也接受已经以 /openai/v1 结尾的 URL。
// 先 Trim 一次,就不用再单独判断结尾的斜杠了。
var trimmed = candidate.TrimEnd('/');
if (trimmed.EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase))
{
return trimmed + "/";
}
return $"{uri.GetLeftPart(UriPartial.Authority).TrimEnd('/')}/openai/v1/";
}
这个辅助方法还强制要求 HTTPS。与其等第一次请求模型时才发现地址有问题,不如在创建会话之前就拒绝掉格式错误的端点。
第 3 步:默认使用 Entra ID 认证
默认的认证方式走 Azure CLI 登录:
if (string.IsNullOrWhiteSpace(apiKey))
{
// AzureCliCredential 会复用 `az login` 建立的会话。
var credential = new AzureCliCredential();
var scope = "https://cognitiveservices.azure.com/.default";
#pragma warning disable GHCP001 // BYOK 认证回调在 Copilot SDK 中仍属实验性功能。
provider.BearerTokenProvider = async _ =>
{
var token = await credential.GetTokenAsync(
new TokenRequestContext([scope]),
cancellation.Token);
return token.Token;
};
#pragma warning restore GHCP001
登录之前,先确认你的身份在对应资源上拥有 Cognitive Services OpenAI User 角色。然后运行:
az login
BearerTokenProvider 是一个回调。SDK 需要令牌时会调用它,而不用接收一个由应用自己管理的固定令牌值。
第 4 步:配置了 API key 时使用它
有些部署或环境仍然要求用 API key。示例代码支持这种方式,并且不需要改动 provider 的其他配置:
else
{
provider.Headers = new Dictionary<string, string>
{
["api-key"] = apiKey
};
}
设置这个可选的密钥:
dotnet user-secrets set "AzureOpenAI:ApiKey" "<your-api-key>"
只要这个值存在,示例就会使用 api-key 请求头,不再需要 az login。控制台输出只报告认证模式,绝不会打印端点或凭据。
别把凭据提交进源码仓库
本地开发用 User Secrets,已部署的应用用环境变量或平台配置管理;不要把 API key 粘进源文件或 README 里。
第 5 步:启动 Copilot 运行时
provider 负责控制模型推理,但 Copilot 运行时仍然不可或缺:
await using var client = new CopilotClient(new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio("copilot"),
WorkingDirectory = AppContext.BaseDirectory
});
await client.StartAsync(cancellation.Token);
这里有个重要区别:BYOK 并不是“不用 Copilot 的 Copilot SDK”。本地 Copilot CLI 运行时仍然负责编排流程,BYOK 改变的只是模型请求发往哪里。
前置条件
安装 .NET 10 SDK 和 GitHub Copilot CLI,确认 dotnet --version 显示的是 .NET 10 SDK,且 copilot --version 能正常运行。如果走默认的 Microsoft Entra ID 认证方式,还需要安装 Azure CLI,确认 az --version 可用。使用 API key 的方式则不需要 Azure CLI。
示例通过 RuntimeConnection.ForStdio("copilot") 启动本地 Copilot 运行时。
第 6 步:创建一个刻意精简的会话
会话配置把模型名称和提供商连起来,然后关掉这个入门示例用不到的功能:
// 关闭工具、技能和 Git 操作,只保留模型请求这一条路径
await using var session = await client.CreateSessionAsync(new SessionConfig
{
ClientName = "github-copilot-sdk-byok",
Model = modelName,
Provider = provider,
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = "Answer every user question in English. Be concise and factual."
},
AvailableTools = [],
Tools = [],
EnableConfigDiscovery = false,
EnableSkills = false,
EnableHostGitOperations = false
}, cancellation.Token);
清空工具集合、关掉这些功能是有意为之。学习提供商配置的过程中,这样能让示例更容易理解,运行起来也更安全。系统消息则让输出保持稳定,方便这个多语言实操示例演示。SystemMessageMode.Replace 会替换掉默认的系统消息,而不是在后面追加一条新指令。
第 7 步:发送一个问题
最后,应用发送一条提示词,等待最终回复:
var response = await session.SendAndWaitAsync(
new MessageOptions { Prompt = question },
TimeSpan.FromMinutes(3),
cancellation.Token);
var content = response?.Data.Content;
if (string.IsNullOrWhiteSpace(content))
{
throw new InvalidOperationException("The model returned an empty response.");
}
Console.WriteLine();
Console.WriteLine("Answer:");
Console.WriteLine(content);
默认问题是:Microsoft Foundry 是什么,它和 GitHub Copilot SDK 有什么关系?你也可以通过命令行参数传入别的问题:
dotnet run -- "用一句话解释 BYOK。"
运行示例
在示例目录下,先配置好必需的值:
dotnet user-secrets set "AzureOpenAI:Endpoint" "https://<resource>.services.ai.azure.com"
dotnet user-secrets set "AzureOpenAI:Deployment" "<your-deployment-name>"
然后走默认的 Entra ID 认证方式:
az login
dotnet run
输出大致如下:
Model: <your-deployment-name>
Authentication: AzureCliCredential
Question: What is Microsoft Foundry, and how does it relate to the GitHub Copilot SDK?
Answer:
<模型响应>示例会打印选中的模型和认证方式,但不会显示端点或凭据。那么,我们到底从中学到了什么?说实话,我最喜欢的部分就是它需要的代码量如此之少。Copilot SDK 运行时和模型提供者是两回事,一旦想通了这一点,剩下的就简单了。你保留 GitHub 提供的运行时:客户端、会话、系统消息和代理管道。然后只需把推理指向别处。那个“别处”可以是你的 Foundry 部署、你的 OpenAI 密钥、你的本地 Ollama,随你需要。Entra ID 是这里的默认路径,我认为也应该如此。没有密钥需要轮换,没有密钥会泄露,没有密钥会被误提交。az login 一下就完事了。API 密钥分支留给确实需要它的场景,在你配置之前它不会碍事。会话也是刻意保持简单。没有工具、没有技能、没有 MCP、没有 Git 访问。这是故意的:第一次运行只有一个任务,就是证明你的模型能回答问题。一旦你在控制台看到那个回答,你就有了一个可用的基线。这才是好玩的地方。从这里开始,你可以一次添加一样东西:工具、MCP 服务器、技能、审批和流式传输。出问题时,你就能准确知道是哪一块坏了。拿你自己的部署去试试吧。
了解更多
GitHubCopilotSDK-01-BYOK 示例
Generative AI for Beginners .NET
GitHub Copilot SDK
Let's Learn .NET
Let's Learn .NET 活动页面
用 GitHub Copilot SDK 构建代理式 AI 应用(Coursera 专业证书)
Microsoft Foundry
探索完整的 GitHub Copilot SDK 示例
快乐编码!Bruno